diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx deleted file mode 100644 index 890bf10..0000000 --- a/src/components/Sidebar/Sidebar.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import React from 'react' - -import styles from './SidebarStyles/Sidebar.module.css' -import SidebarItem from './SidebarItem'; -import useSidebarFromPageConfigs from '../../hooks/useSidebar'; -import { useSidebar as useGenericSidebar } from '../../core/PageManager/SidebarProvider'; -import SidebarUser from './SidebarUser'; -import { useSidebarLogic } from './sidebarLogic'; -import { SidebarProps } from './sidebarTypes'; -import { GoSidebarExpand, GoSidebarCollapse } from 'react-icons/go'; - - -const Sidebar: React.FC = ({ data }) => { - const sidebar = useSidebarLogic(); - - // Ensure data is always an array - const sidebarItems = Array.isArray(data) ? data : []; - - return ( -
-
-
-
- Power - On -
-
- - {/* Minimize/Expand Toggle Button */} - -
- -
- {sidebarItems.map(item => { - return ( - sidebar.toggleItem(item.id)} - isActive={sidebar.isItemActive(item.link)} - isMinimized={sidebar.state.isMinimized} - /> - ); - })} -
- - -
- ) -} - -const SidebarWithData: React.FC = () => { - // Try to use the generic sidebar first, fallback to old system - let sidebarData, isLoading, error; - - try { - const genericSidebar = useGenericSidebar(); - sidebarData = genericSidebar.sidebarItems; - isLoading = genericSidebar.loading; - error = genericSidebar.error; - } catch { - // Fallback to old system if generic sidebar is not available - const oldSidebar = useSidebarFromPageConfigs(); - sidebarData = oldSidebar.items; - isLoading = oldSidebar.isLoading; - error = null; - } - - if (isLoading) { - return ( -
-
-
-
- Power - On -
-
-
-
-
- Loading... -
-
-
- ); - } - - if (error) { - return ( -
-
-
-
- Power - On -
-
-
-
-
- Error: {error} -
-
-
- ); - } - - return ; -}; - -export default SidebarWithData; \ No newline at end of file diff --git a/src/components/Sidebar/SidebarItem.tsx b/src/components/Sidebar/SidebarItem.tsx deleted file mode 100644 index b8fd59b..0000000 --- a/src/components/Sidebar/SidebarItem.tsx +++ /dev/null @@ -1,650 +0,0 @@ -import React, { useEffect, useRef, useState } from "react"; -import { Link, useLocation } from "react-router-dom"; -import { IoIosArrowDown } from "react-icons/io"; -import { motion, AnimatePresence } from "framer-motion"; - -import styles from './SidebarStyles/SidebarItem.module.css'; -import { SidebarItemProps } from "./sidebarTypes"; - -const SidebarItem: React.FC = React.memo(({ - item, - isOpen, - onToggle, - isActive, - isMinimized -}) => { - const location = useLocation(); - const Icon = item.icon as React.ComponentType>; - const hasSubItems = item.submenu && item.submenu.length > 0; - const isDisabled = item.moduleEnabled === false; - const iconContainerRef = useRef(null); - const depth = item.depth || 0; // Get depth from item, default to 0 - // CSS already has 27px padding on .menu li, so we only add extra for nested items - // Depth 0: 0px extra (uses CSS default 27px), Depth 1: 20px extra, Depth 2: 40px extra - const indentPx = depth === 0 ? 0 : (depth * 20); - - // Local state for nested submenus (not tracked by sidebar logic) - const [nestedOpenStates, setNestedOpenStates] = useState>({}); - - // Check if a submenu item is active - const isSubmenuItemActive = (itemPath?: string) => { - if (!itemPath) return false; - const currentPath = location.pathname; - // Exact match or prefix match at path segment boundary - if (currentPath === itemPath) return true; - if (currentPath.startsWith(itemPath)) { - const nextChar = currentPath[itemPath.length]; - if (nextChar === '/' || nextChar === undefined) return true; - } - return false; - }; - - // Toggle nested submenu - const toggleNestedSubmenu = (itemId: string) => { - setNestedOpenStates(prev => ({ - ...prev, - [itemId]: !prev[itemId] - })); - }; - - // Fix SVG dimensions when minimized - react-icons uses 1em which can be invisible - useEffect(() => { - if (isMinimized && iconContainerRef.current) { - const wrapper = iconContainerRef.current; - const svg = wrapper.querySelector('svg'); - if (svg) { - // Force explicit pixel dimensions - svg.setAttribute('width', '25'); - svg.setAttribute('height', '25'); - svg.style.cssText = 'width: 25px !important; height: 25px !important; display: block !important;'; - - // Get the actual color from parent li element - const parentLi = wrapper.closest('li'); - - // Force color directly - use black for now to ensure visibility - const iconColor = '#000000'; // Force black for visibility - svg.style.setProperty('color', iconColor, 'important'); - svg.style.setProperty('fill', iconColor, 'important'); - svg.style.setProperty('stroke', iconColor, 'important'); - svg.setAttribute('fill', iconColor); - svg.setAttribute('stroke', iconColor); - - // Set fill/stroke on all paths - use black - const paths = svg.querySelectorAll('path'); - paths.forEach(path => { - const originalFill = path.getAttribute('fill'); - const originalStroke = path.getAttribute('stroke'); - const strokeWidth = path.getAttribute('stroke-width'); - - if (originalFill === 'none' || (!originalFill && originalStroke)) { - path.removeAttribute('fill'); - path.setAttribute('stroke', iconColor); - if (!strokeWidth || strokeWidth === '0') { - path.setAttribute('stroke-width', '2'); - } - path.style.setProperty('stroke', iconColor, 'important'); - path.style.setProperty('stroke-width', '2px', 'important'); - path.style.setProperty('fill', 'none', 'important'); - } else { - path.removeAttribute('fill'); - path.setAttribute('fill', iconColor); - path.style.setProperty('fill', iconColor, 'important'); - path.style.setProperty('stroke', iconColor, 'important'); - } - }); - - // Debug: Check if wrapper is visible - const wrapperRect = wrapper.getBoundingClientRect(); - const wrapperComputed = window.getComputedStyle(wrapper); - const button = wrapper.parentElement?.querySelector('button'); - const buttonComputed = button ? window.getComputedStyle(button) : null; - const svgRect = svg.getBoundingClientRect(); - const svgComputed = window.getComputedStyle(svg); - const firstPath = paths[0]; - const pathComputed = firstPath ? window.getComputedStyle(firstPath) : null; - const parentLiComputed = parentLi ? window.getComputedStyle(parentLi) : null; - const parentMenu = wrapper.closest(`.${styles.menu}`); - const parentMenuComputed = parentMenu ? window.getComputedStyle(parentMenu) : null; - - // Check what's actually at the icon position - const centerX = wrapperRect.left + wrapperRect.width / 2; - const centerY = wrapperRect.top + wrapperRect.height / 2; - const elementsAtPoint = document.elementsFromPoint(centerX, centerY); - const wrapperInElements = elementsAtPoint.includes(wrapper); - - console.log(`[${item.name}] Icon wrapper check:`, { - hasSubItems, - wrapperVisible: wrapperComputed.visibility === 'visible', - wrapperOpacity: wrapperComputed.opacity, - wrapperZIndex: wrapperComputed.zIndex, - wrapperDisplay: wrapperComputed.display, - wrapperBackgroundColor: wrapperComputed.backgroundColor, - wrapperRect: wrapperRect ? `width: ${wrapperRect.width}, height: ${wrapperRect.height}, top: ${wrapperRect.top}, left: ${wrapperRect.left}` : 'no rect', - svgExists: !!svg, - svgRect: svgRect ? `width: ${svgRect.width}, height: ${svgRect.height}, top: ${svgRect.top}, left: ${svgRect.left}` : 'no rect', - svgDisplay: svgComputed.display, - svgVisibility: svgComputed.visibility, - svgOpacity: svgComputed.opacity, - svgColor: svgComputed.color, - pathsCount: paths.length, - firstPathFill: pathComputed?.fill, - firstPathOpacity: pathComputed?.opacity, - buttonExists: !!button, - buttonZIndex: buttonComputed?.zIndex, - buttonPosition: buttonComputed?.position, - buttonRect: button ? button.getBoundingClientRect() : null, - elementsAtIconCenter: elementsAtPoint.slice(0, 5).map(el => { - let className: string = ''; - if (typeof el.className === 'string') { - className = el.className; - } else if (el.className && typeof el.className === 'object' && 'baseVal' in el.className) { - className = (el.className as { baseVal?: string }).baseVal || ''; - } else if (el.className) { - className = String(el.className); - } - return { - tag: el.tagName, - class: className?.split(' ')[0] || 'no-class', - zIndex: window.getComputedStyle(el).zIndex - }; - }), - wrapperInElements: wrapperInElements, - parentLiOverflow: parentLiComputed?.overflow, - parentLiClipPath: parentLiComputed?.clipPath, - parentMenuOverflow: parentMenuComputed?.overflow, - parentMenuClipPath: parentMenuComputed?.clipPath - }); - } else { - console.error(`[${item.name}] SVG NOT FOUND in wrapper!`); - } - } - }, [isMinimized, isActive, item.name, hasSubItems]); - - const toggleSubmenu = (e: React.MouseEvent) => { - if (isDisabled) { - e.preventDefault(); - return; - } - e.preventDefault(); - e.stopPropagation(); - onToggle(); - }; - - const handleLinkClick = (e: React.MouseEvent) => { - if (isDisabled) { - e.preventDefault(); - return; - } - // If item has submenu or no link (navigation node), prevent navigation and only toggle submenu - if (hasSubItems || !item.link) { - e.preventDefault(); - e.stopPropagation(); - onToggle(); - return; - } - // Allow normal navigation for items without submenu and with a link - }; - - // Render recursive submenu items - const renderSubmenuItems = () => { - if (!hasSubItems || !item.submenu) return null; - - if (isMinimized) { - // Horizontal layout for minimized sidebar - recursive for all levels - return ( - - {isOpen && ( - 0 ? 0.05 : 0.1 - } - }} - exit={{ - opacity: 0, - transition: { - duration: 0.3, - ease: [0.25, 0.1, 0.25, 1] - } - }} - className={styles.submenuHorizontalContainer} - > -
    - {item.submenu.map((subitem, index) => { - const SubIcon = subitem.icon as React.ComponentType>; - const subIsActive = isSubmenuItemActive(subitem.link); - const hasNestedSubmenu = subitem.submenu && subitem.submenu.length > 0; - const subIsOpen = nestedOpenStates[subitem.id] || false; - - // In minimized mode, items with nested submenus should render as icon buttons - // Their submenu will expand below them when clicked - if (hasNestedSubmenu) { - return ( - 0 ? 0.05 : 0.1) + (index * 0.02) - } - }} - exit={{ - opacity: 0, - transition: { - duration: 0.25, - ease: [0.25, 0.1, 0.25, 1] - } - }} - > - - {/* Render nested submenu horizontally when expanded */} - {subIsOpen && ( - - -
      - {subitem.submenu?.map(nestedSubitem => { - const NestedIcon = nestedSubitem.icon as React.ComponentType>; - const nestedIsActive = isSubmenuItemActive(nestedSubitem.link); - - return ( - - - {NestedIcon && ( - - )} - - - ); - })} -
    -
    -
    - )} -
    - ); - } - - // Regular item without nested submenu - render as icon link with smooth animation - return ( - 0 ? 0.05 : 0.1) + (index * 0.02) - } - }} - exit={{ - opacity: 0, - transition: { - duration: 0.25, - ease: [0.25, 0.1, 0.25, 1] - } - }} - > - - {SubIcon && ( - - )} - - - ); - })} -
-
- )} -
- ); - } else { - // Vertical layout for expanded sidebar - return ( - - {isOpen && ( - - - {/* Render items directly as flex column children - same structure as first level */} - {/* This allows nested submenus to push siblings down just like first level */} - {item.submenu.map(subitem => { - const subIsActive = isSubmenuItemActive(subitem.link); - const subIsOpen = nestedOpenStates[subitem.id] || false; - const subDepth = subitem.depth !== undefined ? subitem.depth : (depth === 0 ? 1 : depth + 1); - - const sidebarItemData = { - id: subitem.id, - name: subitem.name, - link: subitem.link, - icon: subitem.icon, - submenu: subitem.submenu, - moduleEnabled: true, - depth: subDepth - }; - - // Render ALL items as recursive SidebarItem - direct children of flex column - // This matches the first-level structure where SidebarItems are direct siblings - return ( - toggleNestedSubmenu(subitem.id)} - isActive={subIsActive} - isMinimized={isMinimized} - /> - ); - })} - - - )} - - ); - } - }; - - return ( -
-
  • - {/* Icon and text container with indentation - takes remaining space */} -
    - {/* Icon - always render, CSS handles positioning */} - {Icon && !isMinimized && ( - - )} - - {/* Text - hidden when minimized */} - {!isMinimized && ( - <> - {hasSubItems || !item.link ? ( - // For items with submenu or navigation nodes (no link) - - {item.name} - - ) : ( - // For items without submenu and with a link - - {item.name} - - )} - - )} -
    - - {/* Expand button - always right-aligned, not indented */} - {!isMinimized && (hasSubItems || !item.link) && ( - - )} - - {/* Link wrapper for items with link and no submenu - spans full width but text is indented */} - {!isMinimized && !hasSubItems && item.link && ( - - )} - - - {/* Icon for minimized state - render directly as child of li */} - {Icon && isMinimized && ( -
    - -
    - )} - - {/* Clickable overlay for items without submenu and with a link */} - {isMinimized && !isDisabled && !hasSubItems && item.link && ( - - )} - - {/* Clickable overlay for items with submenu or navigation nodes (no link) */} - {isMinimized && (hasSubItems || !item.link) && !isDisabled && ( -
  • - {/* Recursive submenu rendering */} - {hasSubItems && !isDisabled && renderSubmenuItems()} -
    - ); -}); - -SidebarItem.displayName = 'SidebarItem'; - -export default SidebarItem; diff --git a/src/components/Sidebar/SidebarStyles/Sidebar.module.css b/src/components/Sidebar/SidebarStyles/Sidebar.module.css deleted file mode 100644 index 0bf7b94..0000000 --- a/src/components/Sidebar/SidebarStyles/Sidebar.module.css +++ /dev/null @@ -1,182 +0,0 @@ -/* Allgemeine Stile */ -.sidebarContainer { - border-radius: 0px; - background: var(--color-bg); - /*background-image: url('../../../styles/assets/bg.jpg'); - background-size: cover; - background-position: center; - background-repeat: no-repeat; - box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);*/ - width: 250px; - padding: 0; - display: flex; - justify-content: flex-start; - align-items: stretch; - flex-direction: column; - height: 100vh; - max-height: 100vh; - overflow: hidden; - transition: width 0.3s ease-in-out; - position: relative; - z-index: 1; - box-sizing: border-box; - - border-right: 1px solid var(--color-primary); -} - -.sidebar { - display: flex; - flex-direction: column; - align-items: flex-start; - flex: 1; - min-height: 0; - font-family: var(--font-family); - overflow-y: auto; - overflow-x: hidden; /* Disable horizontal scrolling */ - padding: 0; - margin: 0; - /* Ensure sidebar can scroll when content exceeds container */ - /* flex: 1 makes it take available space, min-height: 0 allows it to shrink */ - /* Content will expand naturally and trigger scrolling */ -} - -.sidebarContainer.minimized .sidebar { - overflow: visible !important; -} - -.logoContainer { - display: flex; - height: 80px; - padding: 30px 20px 7px 27px; - justify-content: space-between; - align-items: center; - flex-shrink: 0; - position: relative; - box-sizing: border-box; -} - -.logoWrapper { - display: flex; - justify-content: left; - align-items: center; - - opacity: 1; - transition: opacity 0.3s ease-in-out; - - white-space: nowrap; - overflow: hidden; - - position: absolute; - left: 20px; - top: 50%; - - transform: translateY(-50%); /* Center vertically - prevents jumping */ - width: calc(100% - 90px); /* Full width minus button space */ -} - -.logo { - max-width: 80%; - height: auto; - color: var(--color-primary); -} - -.logoText { - font-family: var(--font-family); - font-size: 35px; - display: flex; - align-items: center; - letter-spacing: -0.5px; - font-weight: 200; - -} - -.logoPower { - color: var(--color-text); -} - -.logoOn { - color: var(--color-secondary); - font-weight: 700; -} - -/* Toggle Button Styles */ -.toggleButton { - background: none; - border: none; - border-radius: 10px; - color: var(--color-text); - width: 50px; - height: 50px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: all 0.3s ease; - flex-shrink: 0; - position: absolute; - right: 20px; - - top: 50%; - transform: translateY(-50%); - -} - -.toggleButton:hover { - background: none; - transform: scale(1.05); - top: 50%; - transform: translateY(-50%); -} - -/* Minimized Sidebar Styles */ -.sidebarContainer.minimized { - width: 80px; - overflow: visible !important; -} - -.sidebarContainer.minimized .sidebar { - width: 80px; - align-items: center; - overflow: visible !important; - overflow-y: visible !important; - overflow-x: visible !important; -} - -.sidebarContainer.minimized .logoContainer { - height: 80px; - padding: 15px 10px 7px 10px; - justify-content: center; - box-sizing: border-box; -} - -.sidebarContainer.minimized .logoWrapper { - opacity: 0; - left: 20px; /* Keep same left position */ - top: 50%; - transform: translateY(-50%); /* Keep same vertical centering */ -} - -.sidebarContainer.minimized .menuText { - opacity: 0; /* Same rule as logo - fade out when sidebar minimized */ -} - -.sidebarContainer.minimized .hassubmenu { - opacity: 0; /* Same rule as logo - fade out when sidebar minimized */ -} - -.sidebarContainer.minimized .text_content { - opacity: 0; /* Same rule as logo and menu text - fade out when sidebar minimized */ -} - -.sidebarContainer.minimized .toggleButton { - right: 15px; /* Center in 80px width: (80-50)/2 = 15px */ - top: 50%; - transform: translateY(-50%); /* Keep same vertical centering */ -} - - - - - - - diff --git a/src/components/Sidebar/SidebarStyles/SidebarItem.module.css b/src/components/Sidebar/SidebarStyles/SidebarItem.module.css deleted file mode 100644 index 7f496cc..0000000 --- a/src/components/Sidebar/SidebarStyles/SidebarItem.module.css +++ /dev/null @@ -1,783 +0,0 @@ -.menu { - display: flex; - flex-direction: column; - align-items: flex-start; - position: relative; - margin: 0; - padding: 0; - width: 250px; - min-width: 250px; - max-width: 250px; - overflow: visible; /* Allow nested submenus to expand */ - /* Ensure menu expands to fit content - critical for proper sidebar expansion */ - /* Don't set height - let it be determined by content */ - flex-shrink: 0; /* Prevent menu from shrinking */ - flex-grow: 0; /* Don't grow beyond content */ -} - -.menu.minimized { - position: relative !important; -} - -.menu li { - display: flex; - width: 250px !important; - min-width: 250px; - max-width: 250px; - height: 44px; - padding: 0 3px 0 27px; - margin: 0; - align-items: center; - color: var(--color-text); - list-style: none; - position: relative; - gap: 8px; - box-sizing: border-box; -} - -.menu li:hover, .menu li.active { - background: var(--color-secondary); - color: white; - border-top-right-radius: 25px; - border-bottom-right-radius: 25px; -} - -.menu li:hover a , .menu li.active a { - color: white; - text-decoration: none; -} - - -.menuTextLink { - flex: 1; - text-decoration: none; - color: inherit; - display: flex; - align-items: center; - padding: 0; - margin: 0; - background: none; - border: none; - cursor: pointer; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - min-width: 0; - gap: 5px; - font-family: var(--font-family); - font-size: 0.9rem; - font-style: normal; - font-weight: 500; - line-height: normal; -} - -.menuTextButton { - flex: 1; - text-decoration: none; - color: inherit; - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 8px 0 0; - margin: 0; - background: none; - border: none; - cursor: pointer; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - min-width: 0; - gap: 5px; - font-family: var(--font-family); - font-size: 0.9rem; - font-style: normal; - font-weight: 500; - line-height: normal; - width: 100%; -} - -.menu li:hover .menuTextButton { - color: white; -} - -.arrowButton { - background: none; - border: none; - padding: 4px; - margin-right: 8px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - transition: background-color 0.2s ease; - flex-shrink: 0; - width: 24px; - height: 24px; - color:var(--color-text); -} - -.menu li:hover .arrowButton { - color: white; -} - - - - -.arrowButton:disabled { - cursor: not-allowed; - opacity: 0.6; -} - -.icon { - display: flex; - width: 25px; - height: 25px; - padding: 2.292px 2.3px 2.508px 2.292px; - justify-content: center; - align-items: center; - flex-shrink: 0; - flex-grow: 0; -} - -/* Ensure icon is visible when minimized */ -.menu.minimized li .icon.iconMinimized { - display: flex !important; - opacity: 1 !important; - visibility: visible !important; - position: absolute !important; - z-index: 99999 !important; -} - -.hassubmenu { - width: 20px; - height: 20px; - opacity: 1; - transition: opacity 0.3s ease-in-out; -} - -.rotated { - transform: rotate(180deg); -} - -.menuText { - opacity: 1; - transition: opacity 0.3s ease-in-out; -} - -.minimizedOverlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 1; - pointer-events: auto; - background: transparent; -} - -.minimizedSubmenuToggle { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: none; - border: none; - cursor: pointer; - z-index: 1; - padding: 0; - margin: 0; - pointer-events: auto; -} - -/* Minimized Menu Styles */ -.menu.minimized li { - width: 46px; - padding: 0; - justify-content: center; - align-items: center; - position: relative !important; - overflow: visible !important; - clip-path: none !important; - clip: none !important; - contain: none !important; - isolation: auto !important; - /* Don't create stacking context that interferes */ - transform: none !important; -} -.menu.minimized li a{ - opacity: 0; -} - -/* Ensure icons are never hidden when minimized */ -.menu.minimized li .iconMinimized, -.menu.minimized li [data-debug="icon-minimized"] { - opacity: 1 !important; - visibility: visible !important; - display: flex !important; - position: absolute !important; - z-index: 99999 !important; -} - -.iconMinimizedWrapper { - position: absolute !important; - top: 50% !important; - left: 50% !important; - transform: translate(-50%, -50%) !important; - font-size: 25px !important; - line-height: 25px !important; - width: 25px !important; - height: 25px !important; - min-width: 25px !important; - min-height: 25px !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; - pointer-events: none !important; - overflow: visible !important; - visibility: visible !important; - opacity: 1 !important; - flex-shrink: 0 !important; - margin: 0 !important; - z-index: 2 !important; -} - -.iconMinimized { - margin: 0 !important; - display: flex !important; - justify-content: center !important; - align-items: center !important; - width: 25px !important; - height: 25px !important; - padding: 2.292px 2.3px 2.508px 2.292px !important; - color: var(--color-text) !important; - opacity: 1 !important; - visibility: visible !important; - font-size: 25px !important; -} - -.iconMinimized svg { - width: 25px !important; - height: 25px !important; - min-width: 25px !important; - min-height: 25px !important; - max-width: 25px !important; - max-height: 25px !important; - display: block !important; - opacity: 1 !important; - visibility: visible !important; -} - -.iconMinimized svg path { - fill: inherit !important; - stroke: inherit !important; -} - -.menu.minimized li.active .iconMinimized svg, -.menu.minimized li:hover .iconMinimized svg { - color: white !important; - fill: white !important; - stroke: white !important; -} - -.menu.minimized li.active .iconMinimized svg path, -.menu.minimized li:hover .iconMinimized svg path { - fill: white !important; - stroke: white !important; -} - -/* Ensure submenu can expand below minimized items */ -.menu.minimized { - width: 100%; - align-items: center; - overflow: visible !important; - min-height: auto !important; - height: auto !important; -} - - - - -.menu.minimized li:hover, -.menu.minimized li.active { - border-radius: 15px; - color: var(--color-bg); -} - -.menu.minimized li.active .iconMinimized, -.menu.minimized li:hover .iconMinimized { - color: white !important; -} - -.menu.minimized li.active .iconMinimized svg, -.menu.minimized li:hover .iconMinimized svg { - color: white !important; - fill: white !important; -} - -.menu.minimized li.active .iconMinimized svg path, -.menu.minimized li:hover .iconMinimized svg path { - fill: white !important; - stroke: white !important; -} - -/* Disabled item styles */ -.menu.disabled, -.menu li.disabledItem { - opacity: 0.4; - pointer-events: none; -} - -.menu li.disabledItem:hover { - background: transparent; - color: var(--color-text); - cursor: not-allowed; -} - -.disabledLink { - color: var(--color-text) !important; - opacity: 0.6 !important; - cursor: not-allowed !important; - pointer-events: none !important; -} - -.disabledText { - color: var(--color-text) !important; - opacity: 0.6 !important; -} - -.disabledIcon { - opacity: 0.6 !important; - color: var(--color-text) !important; -} - -.disabledArrow { - opacity: 0.6 !important; - color: var(--color-text) !important; -} - -/* Ensure disabled items don't show hover effects */ -.menu li.disabledItem:hover .disabledLink, -.menu li.disabledItem:hover .disabledText, -.menu li.disabledItem:hover .disabledIcon, -.menu li.disabledItem:hover .disabledArrow { - color: var(--color-text) !important; - opacity: 0.6 !important; -} - -/* ============================================ - Submenu Styles (merged from SidebarSubmenu.module.css) - ============================================ */ - -.submenu { - position: relative; - border: none; - border-top-right-radius: 25px; - border-bottom-right-radius: 25px; - margin: 0; - overflow: visible; /* Allow nested submenus to expand beyond this container */ - width: 250px !important; - min-width: 250px !important; - max-width: 250px !important; - box-sizing: border-box; - flex-shrink: 0; - flex-grow: 0; - display: block; /* Ensure it's part of the document flow */ - /* Ensure submenu expands parent container */ - height: auto; - min-height: 0; -} - -/* Motion div inside submenu - allow nested submenus to expand */ -.submenu > div[style*="overflow"] { - overflow: visible !important; /* Override inline overflow:hidden for nested submenus */ -} - -/* For nested submenus specifically, ensure they can expand */ -.submenuList li .menu .submenu { - overflow: visible !important; -} - -.submenuList li .menu .submenu > div { - overflow: visible !important; -} - -.submenuLineContainer { - display: flex; /* Flex column - same as .sidebar for consistent behavior */ - flex-direction: column; /* Stack items vertically */ - align-items: flex-start; /* Match .sidebar alignment */ - padding: 0; - margin: 0; - overflow: visible; /* Allow nested submenus to expand */ - width: 100%; /* Full width */ -} - -.submenuList { - margin: 0; - flex-grow: 1; - list-style: none; - padding: 0; - overflow: visible; /* Allow nested submenus to expand beyond container */ -} - -.submenuList li { - width: 100%; - color: #181818; - margin: 0; - position: relative; - overflow: visible; /* Allow nested submenus to expand */ - display: block; /* Block display allows nested submenus to extend properly */ -} - -.submenuList li a { - width: 100%; - height: 100%; - padding: 0 3px 0 27px; /* Base padding, indentation is added via inline styles on parent div */ - background: none; - border: none; - text-align: left; - cursor: pointer; - font-family: var(--font-family); - font-size: 0.9rem; - color: #181818; - display: flex; - align-items: center; - gap: 8px; - text-decoration: none; - transition: background-color 0.2s ease; -} - -.submenuList li a:hover { - background-color: var(--color-hover, rgba(0, 0, 0, 0.05)); -} - -.textContainer { - position: relative; - width: 100%; - overflow: hidden; - white-space: nowrap; -} - -.submenuIcon { - width: 16px; - height: 16px; - color: #181818; - flex-shrink: 0; -} - -/* Horizontal layout for minimized sidebar submenus */ -.submenu.minimized { - width: 100% !important; - margin: 0; - padding: 4px 0; - position: relative; - border-radius: 0; - overflow: hidden !important; - display: flex !important; - align-items: center; - justify-content: center; - opacity: 1 !important; - visibility: visible !important; - z-index: 10 !important; -} - -.submenuHorizontalContainer { - display: flex !important; - align-items: center; - justify-content: center; - width: 100%; - padding: 0; - margin: 0; - box-sizing: border-box; - opacity: 1 !important; - visibility: visible !important; -} - -.submenuHorizontalList { - display: flex !important; - flex-direction: row; - align-items: center; - justify-content: center; - gap: 6px; - list-style: none; - padding: 0; - margin: 0; - flex-wrap: wrap; - opacity: 1 !important; - visibility: visible !important; -} - -.submenuHorizontalItem { - display: flex !important; - align-items: center; - justify-content: center; - margin: 0; - padding: 0; - width: auto; - height: auto; - list-style: none; - opacity: 1 !important; - visibility: visible !important; - position: relative; /* Allow nested submenus to position correctly */ -} - -/* Nested SidebarItem in horizontal list - ensure it renders correctly when minimized */ -.submenuHorizontalItem .menu.minimized { - width: 40px !important; - min-width: 40px !important; - max-width: 40px !important; - height: 40px !important; - display: inline-flex !important; - flex-direction: row !important; - align-items: center !important; - justify-content: center !important; - padding: 0 !important; - margin: 0 !important; -} - -.submenuHorizontalItem .menu.minimized li { - width: 40px !important; - min-width: 40px !important; - max-width: 40px !important; - height: 40px !important; - padding: 0 !important; - margin: 0 !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; -} - -/* Nested horizontal submenu should appear below the parent item */ -.submenuHorizontalItem .submenuHorizontalContainer { - position: absolute; - top: 100%; - left: 0; - margin-top: 4px; - z-index: 100; - background: var(--color-bg); - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - padding: 4px; - transform-origin: top left; - will-change: transform, opacity; -} - -/* Smooth transitions for horizontal submenu items */ -.submenuHorizontalItem { - transition: opacity 0.2s ease; -} - -.submenuHorizontalLink { - transition: background-color 0.2s ease; -} - -.submenuHorizontalLink { - display: flex !important; - align-items: center !important; - justify-content: center !important; - width: 40px; - height: 40px; - border-radius: 12px; - background: none; - border: none; - cursor: pointer; - transition: background-color 0.2s ease; - text-decoration: none; - color: #181818 !important; - padding: 0; - margin: 0; - box-sizing: border-box; - position: relative; - overflow: visible; - opacity: 1 !important; - visibility: visible !important; -} - -.submenuHorizontalLink:hover { - background-color: var(--color-secondary); - color: white; -} - -.submenuHorizontalLink:hover .submenuHorizontalIcon { - color: white !important; -} - -.submenuHorizontalLink:hover .submenuHorizontalIcon svg { - color: white !important; -} - -.submenuHorizontalLink:hover .submenuHorizontalIcon svg path { - fill: currentColor !important; - stroke: currentColor !important; -} - -.submenuHorizontalIcon { - display: block !important; - width: 16px !important; - height: 16px !important; - padding: 0; - flex-shrink: 0; - flex-grow: 0; - color: #181818 !important; - margin: 0; - box-sizing: border-box; - position: relative !important; - top: auto !important; - left: auto !important; - transform: none !important; - opacity: 1 !important; - visibility: visible !important; -} - -.submenuHorizontalIcon svg { - width: 16px !important; - height: 16px !important; - min-width: 16px !important; - min-height: 16px !important; - max-width: 16px !important; - max-height: 16px !important; - display: block !important; - margin: 0; - padding: 0; - opacity: 1 !important; - visibility: visible !important; - color: #181818 !important; -} - -.submenuHorizontalIcon svg path { - color: inherit !important; - fill: currentColor !important; - stroke: currentColor !important; -} - -/* Ensure submenu content is visible when sidebar is minimized */ -.menu.minimized .submenu.minimized, -.menu.minimized .submenu.minimized * { - opacity: 1 !important; - visibility: visible !important; - color: #181818 !important; -} - -.menu.minimized .submenuHorizontalLink, -.menu.minimized .submenuHorizontalLink * { - opacity: 1 !important; - visibility: visible !important; - color: #181818 !important; -} - -/* Active state for submenu items */ -.submenuList li.active { - background-color: var(--color-secondary); - border-radius: 0 25px 25px 0; -} - -.submenuList li.active a, -.submenuList li.active a span, -.submenuList li a.activeLink { - color: white !important; -} - -.submenuList li.active .submenuIcon { - color: white !important; -} - -/* Active state for horizontal (minimized) submenu items */ -.submenuHorizontalItem.active .submenuHorizontalLink, -.submenuHorizontalLink.activeLink { - background-color: var(--color-secondary); - color: white !important; -} - -.submenuHorizontalItem.active .submenuHorizontalIcon, -.submenuHorizontalLink.activeLink .submenuHorizontalIcon { - color: white !important; -} - -.submenuHorizontalItem.active .submenuHorizontalIcon svg, -.submenuHorizontalLink.activeLink .submenuHorizontalIcon svg { - color: white !important; -} - -.submenuHorizontalItem.active .submenuHorizontalIcon svg path, -.submenuHorizontalLink.activeLink .submenuHorizontalIcon svg path { - fill: white !important; - stroke: white !important; -} - -/* Nested submenu toggle button (for navigation nodes without links) */ -.nestedToggleButton { - width: 100%; - height: 100%; - padding: 0 3px 0 27px; - background: none; - border: none; - text-align: left; - cursor: pointer; - font-family: var(--font-family); - font-size: 0.9rem; - color: #181818; - display: flex; - align-items: center; - gap: 8px; - transition: background-color 0.2s ease; -} - -.nestedToggleButton:hover { - background-color: var(--color-hover, rgba(0, 0, 0, 0.05)); -} - -.nestedToggleButton:focus { - outline: none; -} - -/* Nested arrow icon */ -.nestedArrow { - transition: transform 0.2s ease; - flex-shrink: 0; - color: #181818; -} - -.nestedArrow.rotated { - transform: rotate(180deg); -} - -/* Active state for nested toggle button */ -.submenuList li.active .nestedToggleButton { - color: white !important; -} - -.submenuList li.active .nestedArrow { - color: white !important; -} - -/* Nested SidebarItem within submenu - ensure full width and allow submenu to extend */ -.submenuList li .menu { - width: 100% !important; - min-width: 100% !important; - max-width: 100% !important; - overflow: visible !important; /* Allow nested submenus to extend */ -} - -.submenuList li .menu li { - width: 100% !important; - min-width: 100% !important; - max-width: 100% !important; - height: 44px; /* Set height only on the actual item li, not the wrapper */ - display: flex !important; /* Ensure the item content displays correctly */ - align-items: center !important; -} - -/* Ensure nested submenus can expand and are visible */ -.submenuList li .menu .submenu { - position: relative; - overflow: visible !important; - z-index: 10; /* Ensure nested submenu appears above other content */ -} - -/* Allow the sidebar container to expand when nested items are open */ -.sidebar { - overflow-y: auto; - overflow-x: hidden; -} - diff --git a/src/components/Sidebar/SidebarStyles/SidebarSubmenu.module.css b/src/components/Sidebar/SidebarStyles/SidebarSubmenu.module.css deleted file mode 100644 index fbaa951..0000000 --- a/src/components/Sidebar/SidebarStyles/SidebarSubmenu.module.css +++ /dev/null @@ -1,353 +0,0 @@ -.submenu { - position: relative; - border: none; - border-top-right-radius: 25px; - border-bottom-right-radius: 25px; - margin: 0; - overflow: visible; /* Allow nested submenus to expand beyond this container */ - width: 250px !important; - min-width: 250px !important; - max-width: 250px !important; - box-sizing: border-box; - flex-shrink: 0; - flex-grow: 0; -} - -/* Motion div inside submenu - allow nested submenus to expand */ -.submenu > div[style*="overflow"] { - overflow: visible !important; /* Override inline overflow:hidden for nested submenus */ -} - -/* For nested submenus specifically, ensure they can expand */ -.submenuList li .menu .submenu { - overflow: visible !important; -} - -.submenuList li .menu .submenu > div { - overflow: visible !important; -} - - -.submenuLineContainer { - display: flex; - align-items: stretch; - gap: 0; - padding: 0; - margin: 0; - overflow: visible; /* Allow nested submenus to expand */ -} - -.submenuList { - margin: 0; - flex-grow: 1; - list-style: none; - padding: 0; - overflow: visible; /* Allow nested submenus to expand beyond container */ -} - -.submenuList li { - width: 100%; - height: 44px; - color: #181818; - margin: 0; - position: relative; - overflow: visible; /* Allow nested submenus to expand */ - display: flex; - align-items: center; -} - -.submenuList li a { - width: 100%; - height: 100%; - padding: 0 3px 0 27px; /* Base padding, indentation is added via inline styles on parent div */ - background: none; - border: none; - text-align: left; - cursor: pointer; - font-family: var(--font-family); - font-size: 0.9rem; - color: #181818; - display: flex; - align-items: center; - gap: 8px; - text-decoration: none; - transition: background-color 0.2s ease; -} - -.submenuList li a:hover { - background-color: var(--color-hover, rgba(0, 0, 0, 0.05)); -} - -.textContainer { - position: relative; - width: 100%; - overflow: hidden; - white-space: nowrap; -} - -.submenuIcon { - width: 16px; - height: 16px; - color: #181818; - flex-shrink: 0; -} - -/* Horizontal layout for minimized sidebar submenus */ -.submenu.minimized { - width: 100% !important; - margin: 0; - padding: 4px 0; - position: relative; - border-radius: 0; - overflow: hidden !important; - display: flex !important; - align-items: center; - justify-content: center; - opacity: 1 !important; - visibility: visible !important; - z-index: 10 !important; -} - -.submenuHorizontalContainer { - display: flex !important; - align-items: center; - justify-content: center; - width: 100%; - padding: 0; - margin: 0; - box-sizing: border-box; - opacity: 1 !important; - visibility: visible !important; -} - -.submenuHorizontalList { - display: flex !important; - flex-direction: row; - align-items: center; - justify-content: center; - gap: 6px; - list-style: none; - padding: 0; - margin: 0; - flex-wrap: wrap; - opacity: 1 !important; - visibility: visible !important; -} - -.submenuHorizontalItem { - display: flex !important; - align-items: center; - justify-content: center; - margin: 0; - padding: 0; - width: auto; - height: auto; - list-style: none; - opacity: 1 !important; - visibility: visible !important; -} - -.submenuHorizontalLink { - display: flex !important; - align-items: center !important; - justify-content: center !important; - width: 40px; - height: 40px; - border-radius: 12px; - background: none; - border: none; - cursor: pointer; - transition: background-color 0.2s ease; - text-decoration: none; - color: #181818 !important; - padding: 0; - margin: 0; - box-sizing: border-box; - position: relative; - overflow: visible; - opacity: 1 !important; - visibility: visible !important; -} - -.submenuHorizontalLink:hover { - background-color: var(--color-secondary); - color: white; -} - -.submenuHorizontalLink:hover .submenuHorizontalIcon { - color: white !important; -} - -.submenuHorizontalLink:hover .submenuHorizontalIcon svg { - color: white !important; -} - -.submenuHorizontalLink:hover .submenuHorizontalIcon svg path { - fill: currentColor !important; - stroke: currentColor !important; -} - -.submenuHorizontalIcon { - display: block !important; - width: 16px !important; - height: 16px !important; - padding: 0; - flex-shrink: 0; - flex-grow: 0; - color: #181818 !important; - margin: 0; - box-sizing: border-box; - position: relative !important; - top: auto !important; - left: auto !important; - transform: none !important; - opacity: 1 !important; - visibility: visible !important; -} - -.submenuHorizontalIcon svg { - width: 16px !important; - height: 16px !important; - min-width: 16px !important; - min-height: 16px !important; - max-width: 16px !important; - max-height: 16px !important; - display: block !important; - margin: 0; - padding: 0; - opacity: 1 !important; - visibility: visible !important; - color: #181818 !important; -} - -.submenuHorizontalIcon svg path { - color: inherit !important; - fill: currentColor !important; - stroke: currentColor !important; -} - -/* Ensure submenu content is visible when sidebar is minimized */ -.menu.minimized .submenu.minimized, -.menu.minimized .submenu.minimized * { - opacity: 1 !important; - visibility: visible !important; - color: #181818 !important; -} - -.menu.minimized .submenuHorizontalLink, -.menu.minimized .submenuHorizontalLink * { - opacity: 1 !important; - visibility: visible !important; - color: #181818 !important; -} - -/* Active state for submenu items */ -.submenuList li.active { - background-color: var(--color-secondary); - border-radius: 0 25px 25px 0; -} - -.submenuList li.active a, -.submenuList li.active a span, -.submenuList li a.activeLink { - color: white !important; -} - -.submenuList li.active .submenuIcon { - color: white !important; -} - -/* Active state for horizontal (minimized) submenu items */ -.submenuHorizontalItem.active .submenuHorizontalLink, -.submenuHorizontalLink.activeLink { - background-color: var(--color-secondary); - color: white !important; -} - -.submenuHorizontalItem.active .submenuHorizontalIcon, -.submenuHorizontalLink.activeLink .submenuHorizontalIcon { - color: white !important; -} - -.submenuHorizontalItem.active .submenuHorizontalIcon svg, -.submenuHorizontalLink.activeLink .submenuHorizontalIcon svg { - color: white !important; -} - -.submenuHorizontalItem.active .submenuHorizontalIcon svg path, -.submenuHorizontalLink.activeLink .submenuHorizontalIcon svg path { - fill: white !important; - stroke: white !important; -} - -/* Nested submenu toggle button (for navigation nodes without links) */ -.nestedToggleButton { - width: 100%; - height: 100%; - padding: 0 3px 0 27px; - background: none; - border: none; - text-align: left; - cursor: pointer; - font-family: var(--font-family); - font-size: 0.9rem; - color: #181818; - display: flex; - align-items: center; - gap: 8px; - transition: background-color 0.2s ease; -} - -.nestedToggleButton:hover { - background-color: var(--color-hover, rgba(0, 0, 0, 0.05)); -} - -.nestedToggleButton:focus { - outline: none; -} - -/* Nested arrow icon */ -.nestedArrow { - transition: transform 0.2s ease; - flex-shrink: 0; - color: #181818; -} - -.nestedArrow.rotated { - transform: rotate(180deg); -} - -/* Active state for nested toggle button */ -.submenuList li.active .nestedToggleButton { - color: white !important; -} - -.submenuList li.active .nestedArrow { - color: white !important; -} - -/* Nested SidebarItem within submenu - ensure full width */ -.submenuList li .menu { - width: 100% !important; - min-width: 100% !important; - max-width: 100% !important; -} - -.submenuList li .menu li { - width: 100% !important; - min-width: 100% !important; - max-width: 100% !important; -} - -/* Ensure nested submenus can expand and are visible */ -.submenuList li .menu .submenu { - position: relative; - overflow: visible !important; - z-index: 10; /* Ensure nested submenu appears above other content */ -} - -/* Allow the sidebar container to expand when nested items are open */ -.sidebar { - overflow-y: auto; - overflow-x: hidden; -} \ No newline at end of file diff --git a/src/components/Sidebar/SidebarStyles/SidebarUser.module.css b/src/components/Sidebar/SidebarStyles/SidebarUser.module.css deleted file mode 100644 index 48e9ce2..0000000 --- a/src/components/Sidebar/SidebarStyles/SidebarUser.module.css +++ /dev/null @@ -1,323 +0,0 @@ -.user_section { - display: flex; - width: 100%; - flex-direction: column; - align-items: left; - font-family: var(--font-family); - box-sizing: border-box; - position: relative; - margin-top: auto; /* Push to bottom */ - margin-bottom: 0; - padding-left: 5px; - padding-bottom: 7px; - padding-right: 5px; - flex-shrink: 0; - max-height: fit-content; - overflow: visible; - contain: layout; -} - -.user_info { - display: flex; - flex-direction: left; - align-items: center; - - gap: 12px; - width: 100%; - border-top-right-radius: 25px; - border-bottom-right-radius : 25px; - padding: 0 3px 0 15px; - - opacity: 1; - white-space: nowrap; - overflow: hidden; - transition: opacity 0.3s ease-in-out; - - - -} - -.user_info.notClickable { - cursor: pointer; - padding: 8px; - margin: -8px; - background-color: var(--color-secondary, rgba(0, 0, 0, 0.05)); /* Compensate for padding to maintain original size */ -} - -.user_info.clickable { - cursor: pointer; - padding: 8px; - margin: -8px; - background-color: none; /* Compensate for padding to maintain original size */ -} - -.user_info.clickable:hover { - background-color: var(--color-secondary, rgba(0, 0, 0, 0.05)); -} - -.user_header { - display: flex; - align-items: center; - gap: 12px; - width: 100%; - transition: justify-content 0.3s ease; - padding: 0 3px 0 15px; - -} - -.user_avatar { - width: 40px; - height: 40px; - border-radius: 50%; - background-color: var(--color-primary); - color: var(--color-bg); - display: flex; - align-items: center; - justify-content: center; - font-size: 0.9rem; - font-weight: 600; - text-transform: uppercase; - flex-shrink: 0; - transition: all 0.3s ease; - letter-spacing: 0.5px; -} - -.text_content { - display: flex; - flex-direction: column; - gap: 4px; - overflow: hidden; - - white-space: nowrap; - overflow: hidden; - opacity: 0; /* Start hidden */ - animation: delayedFadeIn 0.3s ease-in-out forwards; /* 0.3s delay + 0.3s fade in */ -} - -.username { - font-size: 0.8rem !important; - color: var(--color-text) !important; - font-style: italic; - - -} - -.logout_popup { - position: absolute; - bottom: 100%; - left: 0px; - right: 10px; - background: var(--color-primary); - border: none; - border-top-right-radius: 25px; - border-bottom-right-radius: 25px; - z-index: 10; - margin-bottom: 8px; - overflow: hidden; -} - -.logout_menu_button { - width: 100%; - padding: 12px 25px; - background: none; - border: none; - text-align: left; - cursor: pointer; - font-family: var(--font-family); - font-size: 0.9rem; - color: #181818; - display: flex; - align-items: center; - gap: 8px; - -} - -.logout_menu_button:hover:not(:disabled) { - background-color: var(--color-hover, rgba(0, 0, 0, 0.05)); -} - -.logout_menu_button:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.logout_icon { - font-size: 0.9rem; - color: var(--color-gray); -} - -.user_section h1 { - margin: 0; - font-size: 0.9rem; - line-height: 1.; - color: var(--color-text); - font-family: var(--font-family); - font-weight: 400; - opacity: 0; /* Start hidden */ - animation: delayedFadeIn 0.3s ease-in-out forwards; /* 0.3s delay + 0.3s fade in */ - white-space: nowrap; - -} - -.user_section p { - margin: 0; - font-family: var(--font-family); - opacity: 0; /* Start hidden */ - animation: delayedFadeIn 0.3s ease-in-out forwards; /* 0.3s delay + 0.3s fade in */ - white-space: nowrap; -} - -.userContainer { - padding: 12px; - text-align: center; - color: transparent; - background-color: transparent; - border: transparent; -} - -/* Minimized User Section Styles */ -.user_section.minimized { - width: 100%; /* Match menu item width */ - padding: 20px 15px 8px 15px; /* Match menu item padding structure */ - align-items: center; - justify-content: center; - color: transparent; - background-color: transparent; - border: transparent; -} - -.user_section.minimized .user_info { - justify-content: center; /* Center the content when minimized */ - width: 100%; - height: 40px; - color: transparent; - background-color: transparent; - border: transparent; - -} - -.user_section.minimized .user_info.clickable:hover { - background-color: transparent; /* Remove orange background on hover when minimized */ -} - -.user_section.minimized .user_info.notClickable { - background-color: transparent; /* Remove orange background on hover when minimized */ -} - -.user_section.minimized .user_header { - justify-content: center; /* Center the avatar when minimized */ - width: 100%; - padding: 0; /* Remove padding to center properly */ -} - -.user_section.minimized .user_avatar { - width: 40px; - height: 40px; -} - -.user_section.minimized .text_content { - opacity: 0; - width: 0; - color: transparent; - background-color: transparent; - border: transparent; - animation: none; /* Disable animation when minimized */ -} - -.user_section.minimized h1 { - opacity: 0; - color: transparent; - background-color: transparent; - border: transparent; - animation: none; /* Disable animation when minimized */ - -} - -.user_section.minimized p { - opacity: 0; - color: transparent; - background-color: transparent; - border: transparent; - animation: none; /* Disable animation when minimized */ -} - -/* Minimized logout popup styles */ -.logout_popup_minimized { - left: 50%; /* Center horizontally over the user icon */ - bottom: calc(100% + 8px); /* Position above the user icon */ - transform: translateX(-50%) translateY(20px); /* Center and start position for animation */ - width: 40px; /* Same size as user avatar */ - height: 40px; - margin-left: 0; - margin-bottom: 0; - border-radius: 50%; /* Make it circular */ - background: var(--color-primary); - border: 1px solid var(--color-primary); - overflow: visible; /* Allow tooltip to show */ - opacity: 0; /* Start invisible */ - animation: flyUpFadeIn 0.3s ease-out forwards; /* Animation */ - transition: opacity 0.2s ease-out; /* Smooth fade out when disappearing */ -} - -.logout_popup_minimized .logout_menu_button { - justify-content: center; - align-items: center; - padding: 0; - width: 100%; - height: 100%; - border-radius: 50%; /* Make button circular */ - position: relative; -} - -.logout_popup_minimized .logout_menu_button::after { - content: 'Abmelden'; - position: absolute; - bottom: calc(100% + 8px); - left: 50%; - transform: translateX(-50%); - - color: white; - padding: 4px 8px; - border-radius: 4px; - font-size: 0.75rem; - white-space: nowrap; - opacity: 0; - pointer-events: none; - transition: opacity 0.3s ease; - z-index: 11; -} - -.logout_popup_minimized .logout_menu_button:hover::after { - opacity: 1; -} - -.logout_popup_minimized .logout_icon { - margin: 0; - font-size: 1rem; -} - -/* Animation keyframes for delayed text appearance */ -@keyframes delayedFadeIn { - 0% { - opacity: 0; - } - 50% { - opacity: 0; /* Stay hidden for first 50% (0.3s) */ - } - 100% { - opacity: 1; /* Fade in during last 50% (0.3s) */ - } -} - -/* Animation keyframes for fly up and fade in effect */ -@keyframes flyUpFadeIn { - from { - opacity: 0; - transform: translateX(-50%) translateY(20px); - } - to { - opacity: 1; - transform: translateX(-50%) translateY(0); - } -} - diff --git a/src/components/Sidebar/SidebarUser.tsx b/src/components/Sidebar/SidebarUser.tsx deleted file mode 100644 index cc4ae9d..0000000 --- a/src/components/Sidebar/SidebarUser.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react' -import { useMsal } from '@azure/msal-react' -import { FaSignOutAlt } from 'react-icons/fa' -import styles from './SidebarStyles/SidebarUser.module.css' -import { User } from '../../hooks/useUsers' -import { SidebarUserProps } from './sidebarTypes'; -import { getUserDataCache, CachedUserData } from '../../utils/userCache'; -import { useCurrentUser } from '../../hooks/useUsers'; - -const SidebarUser: React.FC = ({ isMinimized = false }) => { - const { instance } = useMsal(); - const { logout } = useCurrentUser(); // Only use logout function from hook - - // Local state for user data created from cached data - const [user, setUser] = useState(null); - const [userError, setUserError] = useState(null); - const [cachedUserData, setCachedUserData] = useState(null); - - const [showLogoutMenu, setShowLogoutMenu] = useState(false); - const [isLoggingOut, setIsLoggingOut] = useState(false); - const userSectionRef = useRef(null); - - - // Function to get initials from full name - const getInitials = (fullName: string): string => { - return fullName - .split(' ') - .map(name => name.charAt(0).toUpperCase()) - .join('') - .substring(0, 2); // Limit to 2 characters - }; - - const handleUserClick = () => { - setShowLogoutMenu(!showLogoutMenu); - }; - - const handleLogout = async () => { - if (!user || isLoggingOut) return; - - setIsLoggingOut(true); - try { - // Pass MSAL instance for Microsoft authentication - if (user.authenticationAuthority === 'msft') { - await logout(instance); - } else { - await logout(); - } - setShowLogoutMenu(false); - } catch (error) { - console.error('Logout failed:', error); - // Keep the menu open if logout fails so user can try again - setIsLoggingOut(false); - } - }; - - // Load cached user data on mount and when user updates - useEffect(() => { - const loadCachedUserData = () => { - const cached = getUserDataCache(); - setCachedUserData(cached); - - if (cached?.id) { - // Create a User object from cached data with fallback values - const userData: User = { - id: cached.id, - username: cached.username, - email: cached.email || cached.username, // Use email or username as fallback - fullName: cached.fullName || cached.username.split('@')[0] || cached.username, - language: cached.language || 'de', // Default language - enabled: cached.enabled ?? true, // Assume enabled if logged in - roleLabels: cached.roleLabels || [], - authenticationAuthority: cached.authenticationAuthority || 'local', - isSysAdmin: cached.isSysAdmin || false - }; - setUser(userData); - setUserError(null); - } else { - setUser(null); - } - }; - - loadCachedUserData(); - }, []); // Empty dependency array - only run on mount - - // Listen for user updates from settings page - useEffect(() => { - const handleUserUpdate = () => { - // Refresh cached user data when user info is updated - const cached = getUserDataCache(); - setCachedUserData(cached); - - if (cached?.id) { - const userData: User = { - id: cached.id, - username: cached.username, - email: cached.email || cached.username, - fullName: cached.fullName || cached.username.split('@')[0] || cached.username, - language: cached.language || 'de', - enabled: cached.enabled ?? true, - roleLabels: cached.roleLabels || [], - authenticationAuthority: cached.authenticationAuthority || 'local', - isSysAdmin: cached.isSysAdmin || false - }; - setUser(userData); - setUserError(null); - } else { - setUser(null); - } - }; - - window.addEventListener('userInfoUpdated', handleUserUpdate); - return () => { - window.removeEventListener('userInfoUpdated', handleUserUpdate); - }; - }, []); // Empty dependency array - only set up listener once - - // Close popup when clicking outside - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (userSectionRef.current && !userSectionRef.current.contains(event.target as Node)) { - setShowLogoutMenu(false); - } - }; - - if (showLogoutMenu) { - document.addEventListener('mousedown', handleClickOutside); - } - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [showLogoutMenu]); - - if (!cachedUserData) { - return ( -
    -
    Lädt...
    -
    - ); - } - - if (userError) { - return ( -
    -
    Fehler beim Laden des Benutzerprofils
    -
    - ); - } - - if (!user) { - return ( -
    -
    Kein Benutzer gefunden
    -
    - ); - } - - return ( -
    - {showLogoutMenu && ( -
    - -
    - )} - -
    -
    -
    - {getInitials(user.fullName)} -
    - {!isMinimized && ( -
    -

    {user.fullName}

    -

    {user.username}

    -
    - )} -
    -
    -
    - ) -} - -export default SidebarUser; \ No newline at end of file diff --git a/src/components/Sidebar/index.ts b/src/components/Sidebar/index.ts deleted file mode 100644 index 38709bc..0000000 --- a/src/components/Sidebar/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Sidebar from "./Sidebar"; - -export default Sidebar; -export { default as SidebarItem } from './SidebarItem'; -export { default as SidebarUser } from './SidebarUser'; -export * from './sidebarTypes'; -export { useSidebarLogic } from './sidebarLogic'; \ No newline at end of file diff --git a/src/components/Sidebar/sidebarLogic.ts b/src/components/Sidebar/sidebarLogic.ts deleted file mode 100644 index 5857fe9..0000000 --- a/src/components/Sidebar/sidebarLogic.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { useState, useCallback } from 'react'; -import { useLocation } from 'react-router-dom'; -import { SidebarState, SidebarContextType } from './sidebarTypes'; - -// Custom hook for sidebar logic -export const useSidebarLogic = (): SidebarContextType => { - const location = useLocation(); - - // Simple React state instead of state machine - const [state, setState] = useState({ - openItemId: null, - isMinimized: false, - }); - - // Toggle a specific menu item - const toggleItem = useCallback((itemId: string) => { - setState(prevState => ({ - ...prevState, - openItemId: prevState.openItemId === itemId ? null : itemId, - })); - }, []); - - // Close all submenus - const closeAll = useCallback(() => { - setState(prevState => ({ - ...prevState, - openItemId: null, - })); - }, []); - - // Check if a specific item is open - const isItemOpen = useCallback((itemId: string) => { - return state.openItemId === itemId; - }, [state.openItemId]); - - // Check if an item is the active route - // Supports exact match and prefix match (for parent items when child route is active) - const isItemActive = useCallback((itemPath?: string) => { - if (!itemPath) return false; - - const currentPath = location.pathname; - - // Exact match - if (currentPath === itemPath) return true; - - // Prefix match: check if current path starts with the item path - // This highlights parent items when a child/subpage is active - // Ensure we match at path segment boundaries (e.g., /admin matches /admin/users but not /administrator) - if (currentPath.startsWith(itemPath)) { - // Check if the next character is either '/' or end of string - const nextChar = currentPath[itemPath.length]; - if (nextChar === '/' || nextChar === undefined) { - return true; - } - } - - return false; - }, [location.pathname]); - - // Minimize sidebar - const minimizeSidebar = useCallback(() => { - setState(prevState => ({ - ...prevState, - isMinimized: true, - // Keep submenus open when minimizing - submenu state is independent - })); - }, []); - - // Expand sidebar - const expandSidebar = useCallback(() => { - setState(prevState => ({ - ...prevState, - isMinimized: false, - })); - }, []); - - return { - state, - toggleItem, - closeAll, - isItemOpen, - isItemActive, - minimizeSidebar, - expandSidebar, - }; -}; \ No newline at end of file diff --git a/src/components/Sidebar/sidebarTypes.ts b/src/components/Sidebar/sidebarTypes.ts deleted file mode 100644 index a09ead2..0000000 --- a/src/components/Sidebar/sidebarTypes.ts +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; - -// Base sidebar item interface -export interface SidebarItemData { - id: string; - name: string; - link?: string; - icon?: React.ComponentType>; - submenu?: SidebarSubmenuItemData[]; - moduleEnabled?: boolean; // New property for module state - depth?: number; // Hierarchy depth for indentation (0 = top level) -} - -// Submenu item interface - supports recursive nesting -export interface SidebarSubmenuItemData { - id: string; - name: string; - link?: string; // Optional - if undefined, it's a navigation node (not a page) - icon?: React.ComponentType>; - submenu?: SidebarSubmenuItemData[]; // Recursive support for nested submenus - depth?: number; // Hierarchy depth for indentation (0 = top level) -} - -// Sidebar state interface -export interface SidebarState { - openItemId: string | null; - isMinimized: boolean; -} - -// Sidebar context interface -export interface SidebarContextType { - state: SidebarState; - toggleItem: (itemId: string) => void; - closeAll: () => void; - isItemOpen: (itemId: string) => boolean; - isItemActive: (itemPath?: string) => boolean; - minimizeSidebar: () => void; - expandSidebar: () => void; -} - -// Component props interfaces -export interface SidebarProps { - data: SidebarItemData[]; -} - -export interface SidebarItemProps { - item: SidebarItemData; - isOpen: boolean; - onToggle: () => void; - isActive: boolean; - isMinimized: boolean; -} - -export interface SidebarSubmenuProps { - item: SidebarItemData; - isOpen: boolean; - isMinimized?: boolean; -} - -export interface SidebarUserProps { - isMinimized?: boolean; -} \ No newline at end of file diff --git a/src/components/Speech/SpeechSettings.tsx b/src/components/Speech/SpeechSettings.tsx deleted file mode 100644 index 3395454..0000000 --- a/src/components/Speech/SpeechSettings.tsx +++ /dev/null @@ -1,481 +0,0 @@ -import { useState, useEffect } from 'react'; -import { IoIosBusiness, IoIosContact, IoIosTime, IoIosRefresh } from 'react-icons/io'; -import sharedStyles from '../../core/PageManager/pages.module.css'; -import styles from './SpeechSettings.module.css'; -import { useLanguage } from '../../providers/language/LanguageContext'; - -interface MandateData { - id: string; - mandate_general: { - company_name: string; - industry: string; - contact_info: { - email: string; - phone: string; - street: string; - postal_code: string; - city: string; - country: string; - }; - business_hours: string; - timezone: string; - }; - setup_contacts: boolean; -} - -interface SpeechSettingsProps { - onDataUpdate?: (data: MandateData) => void; -} - -function SpeechSettings({ onDataUpdate }: SpeechSettingsProps) { - const { t } = useLanguage(); - const [formData, setFormData] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isSaving, setIsSaving] = useState(false); - const [saveMessage, setSaveMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null); - const [focusedFields, setFocusedFields] = useState>(new Set()); - - // Load data from localStorage on component mount - useEffect(() => { - const loadSpeechData = () => { - try { - const savedData = localStorage.getItem('speechSignUpData'); - const timestamp = localStorage.getItem('speechSignUpTimestamp'); - - if (savedData && timestamp) { - const parsedData = JSON.parse(savedData); - const savedTime = parseInt(timestamp); - const now = Date.now(); - const twentyFourHours = 24 * 60 * 60 * 1000; - - // Check if data is still valid (within 24 hours) - if (now - savedTime < twentyFourHours) { - setFormData(parsedData); - } else { - // Data expired, clear it - localStorage.removeItem('speechSignUpData'); - localStorage.removeItem('speechSignUpTimestamp'); - } - } - } catch (error) { - console.error('Error loading speech data:', error); - } finally { - setIsLoading(false); - } - }; - - loadSpeechData(); - }, []); - - const handleInputChange = (field: string, value: string) => { - if (!formData) return; - - const newData = { ...formData }; - const fieldParts = field.split('.'); - - if (fieldParts.length === 2) { - // Handle nested fields like mandate_general.company_name - const [parent, child] = fieldParts; - if (parent === 'mandate_general' && child in newData.mandate_general) { - (newData.mandate_general as any)[child] = value; - } - } else if (fieldParts.length === 3) { - // Handle deeply nested fields like mandate_general.contact_info.email - const [parent, child, grandchild] = fieldParts; - if (parent === 'mandate_general' && child === 'contact_info' && grandchild in newData.mandate_general.contact_info) { - (newData.mandate_general.contact_info as any)[grandchild] = value; - } - } else if (field === 'setup_contacts') { - newData.setup_contacts = value === 'true'; - } - - setFormData(newData); - setSaveMessage(null); - }; - - const handleFocus = (field: string) => { - setFocusedFields(prev => new Set(prev).add(field)); - }; - - const handleBlur = (field: string) => { - setFocusedFields(prev => { - const newSet = new Set(prev); - newSet.delete(field); - return newSet; - }); - }; - - const handleSave = async () => { - if (!formData) return; - - setIsSaving(true); - try { - // Save to localStorage - localStorage.setItem('speechSignUpData', JSON.stringify(formData)); - localStorage.setItem('speechSignUpTimestamp', Date.now().toString()); - - // Dispatch event to notify other components - window.dispatchEvent(new CustomEvent('speechSignUpChanged')); - - setSaveMessage({ type: 'success', text: t('speech.settings.save_success') }); - - // Notify parent component if callback provided - if (onDataUpdate) { - onDataUpdate(formData); - } - - // Clear message after 3 seconds - setTimeout(() => setSaveMessage(null), 3000); - } catch (error) { - console.error('Error saving speech settings:', error); - setSaveMessage({ type: 'error', text: t('speech.settings.save_error') }); - } finally { - setIsSaving(false); - } - }; - - const handleReset = () => { - // Direct reset - user clicked the reset button intentionally - localStorage.removeItem('speechSignUpData'); - localStorage.removeItem('speechSignUpTimestamp'); - window.dispatchEvent(new CustomEvent('speechSignUpChanged')); - setFormData(null); - setSaveMessage({ type: 'success', text: t('speech.settings.reset_success') }); - setTimeout(() => setSaveMessage(null), 3000); - }; - - if (isLoading) { - return ( -
    -
    - {t('common.loading')} -
    -
    - ); - } - - if (!formData) { - return ( -
    -
    -

    {t('speech.settings.no_data')}

    - -
    -
    - ); - } - - return ( -
    -
    -

    {t('speech.settings.title')}

    -

    {t('speech.settings.description')}

    -
    - - {saveMessage && ( -
    - {saveMessage.text} -
    - )} - -
    - {/* Company Information Section */} -
    -
    - -

    {t('speech.settings.company_info')}

    -
    - -
    -
    -
    - handleInputChange('mandate_general.company_name', e.target.value)} - onFocus={() => handleFocus('company_name')} - onBlur={() => handleBlur('company_name')} - required - /> - -
    -
    - -
    -
    - handleInputChange('mandate_general.industry', e.target.value)} - onFocus={() => handleFocus('industry')} - onBlur={() => handleBlur('industry')} - required - /> - -
    -
    -
    -
    - - {/* Contact Information Section */} -
    -
    - -

    {t('speech.settings.contact_info')}

    -
    - -
    -
    -
    - handleInputChange('mandate_general.contact_info.email', e.target.value)} - onFocus={() => handleFocus('email')} - onBlur={() => handleBlur('email')} - required - /> - -
    -
    - -
    -
    - handleInputChange('mandate_general.contact_info.phone', e.target.value)} - onFocus={() => handleFocus('phone')} - onBlur={() => handleBlur('phone')} - required - /> - -
    -
    -
    - -
    -
    -
    - handleInputChange('mandate_general.contact_info.street', e.target.value)} - onFocus={() => handleFocus('street')} - onBlur={() => handleBlur('street')} - required - /> - -
    -
    - -
    -
    - handleInputChange('mandate_general.contact_info.postal_code', e.target.value)} - onFocus={() => handleFocus('postal_code')} - onBlur={() => handleBlur('postal_code')} - required - /> - -
    -
    -
    - -
    -
    -
    - handleInputChange('mandate_general.contact_info.city', e.target.value)} - onFocus={() => handleFocus('city')} - onBlur={() => handleBlur('city')} - required - /> - -
    -
    - -
    -
    - handleInputChange('mandate_general.contact_info.country', e.target.value)} - onFocus={() => handleFocus('country')} - onBlur={() => handleBlur('country')} - required - /> - -
    -
    -
    -
    - - {/* Business Hours Section */} -
    -
    - -

    {t('speech.settings.business_hours')}

    -
    - -
    -
    -
    - handleInputChange('mandate_general.business_hours', e.target.value)} - onFocus={() => handleFocus('business_hours')} - onBlur={() => handleBlur('business_hours')} - required - /> - -
    -
    - -
    -
    - - -
    -
    -
    -
    - - {/* Actions */} -
    - - - -
    -
    -
    - ); -} - -export default SpeechSettings; diff --git a/src/components/UiComponents/Button/ButtonTypes.ts b/src/components/UiComponents/Button/ButtonTypes.ts index 807c9c4..3bee1ee 100644 --- a/src/components/UiComponents/Button/ButtonTypes.ts +++ b/src/components/UiComponents/Button/ButtonTypes.ts @@ -52,5 +52,4 @@ export interface CreateButtonProps extends BaseButtonProps { iconPosition?: 'left' | 'right'; onSuccess?: (result: any) => void; onError?: (error: string) => void; - multiStep?: boolean; // Enable multi-step form mode } \ No newline at end of file diff --git a/src/components/UiComponents/Button/CreateButton/CreateButton.tsx b/src/components/UiComponents/Button/CreateButton/CreateButton.tsx index d60dfd4..a718c98 100644 --- a/src/components/UiComponents/Button/CreateButton/CreateButton.tsx +++ b/src/components/UiComponents/Button/CreateButton/CreateButton.tsx @@ -4,277 +4,8 @@ import Button from '../Button'; import { Popup } from '../../Popup'; import { FormGeneratorForm, AttributeDefinition } from '../../../FormGenerator/FormGeneratorForm'; import { useLanguage } from '../../../../providers/language/LanguageContext'; -import { TextField } from '../../TextField'; -import { PekProvider } from '../../../../contexts/PekContext'; -import { MapView } from '../../MapView'; -import { usePekContext } from '../../../../contexts/PekContext'; -import { FaLocationArrow, FaTimes } from 'react-icons/fa'; -import { IoMdSend } from 'react-icons/io'; import styles from './CreateButton.module.css'; -// Step 2 component for parcel selection (must be inside PekProvider) -const Step2Content: React.FC<{ - onNext: (data: any) => void; - onBack: () => void; - addressData: { - street: string; - postalCode: string; - city: string; - }; - onAddressChange: (field: string, value: string) => void; -}> = ({ onNext, onBack, addressData, onAddressChange }) => { - const { t } = useLanguage(); - const { - selectedParcels, - searchParcel, - useCurrentLocation, - isGettingLocation, - isSearchingParcel, - setAdresse, - mapCenter, - mapZoomBounds, - parcelGeometries, - handleMapClick, - handleParcelClick, - removeParcel, - setIsPanelOpen - } = usePekContext(); - const [step2Errors, setStep2Errors] = useState>({}); - - // Prevent panel from opening when parcel is clicked - React.useEffect(() => { - setIsPanelOpen(false); - }, [selectedParcels, setIsPanelOpen]); - - // Build location string from address fields - const buildLocationString = () => { - const parts = []; - if (addressData.street.trim()) parts.push(addressData.street.trim()); - if (addressData.postalCode.trim()) parts.push(addressData.postalCode.trim()); - if (addressData.city.trim()) parts.push(addressData.city.trim()); - return parts.join(', '); - }; - - const handleSearch = async () => { - const locationString = buildLocationString(); - if (locationString.trim()) { - // Update the adresse field in context for consistency - setAdresse(locationString); - await searchParcel(locationString.trim(), true); - } - }; - - const handleUseCurrentLocation = async () => { - await useCurrentLocation(); - }; - - const handleNext = () => { - const newErrors: Record = {}; - - if (!addressData.street.trim()) { - newErrors.street = t('formgen.form.required', 'Strasse und Hausnummer ist erforderlich'); - } - if (!addressData.postalCode.trim()) { - newErrors.postalCode = t('formgen.form.required', 'Postleitzahl ist erforderlich'); - } - if (!addressData.city.trim()) { - newErrors.city = t('formgen.form.required', 'Stadt ist erforderlich'); - } - if (!selectedParcels || selectedParcels.length === 0) { - newErrors.parcel = t('formgen.form.required', 'Bitte wählen Sie mindestens eine Parzelle aus'); - } - - setStep2Errors(newErrors); - - if (Object.keys(newErrors).length === 0) { - onNext({ - address: addressData, - parzellen: selectedParcels - }); - } - }; - - return ( -
    -
    - 2 - Parzelle hinzufügen -
    - -
    - onAddressChange('street', value)} - label="Strasse und Hausnummer" - placeholder="z.B. Bundesplatz 3" - required - error={step2Errors.street} - size="md" - type="text" - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - handleSearch(); - } - }} - /> - -
    - onAddressChange('postalCode', value)} - label="Postleitzahl" - placeholder="z.B. 3000" - required - error={step2Errors.postalCode} - size="md" - type="text" - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - handleSearch(); - } - }} - /> - - onAddressChange('city', value)} - label="Stadt" - placeholder="z.B. Bern" - required - error={step2Errors.city} - size="md" - type="text" - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - handleSearch(); - } - }} - /> -
    -
    - - {/* Search buttons */} -
    - - -
    - -
    -
    - -
    - - {/* Selected parcels list displayed below map */} - {selectedParcels && selectedParcels.length > 0 && ( -
    -

    Ausgewählte Parzellen ({selectedParcels.length})

    -
    - {selectedParcels.map((selectedParcel, index) => ( -
    -
    -

    - Parzelle {index + 1}: {selectedParcel.parcel.number || selectedParcel.parcel.id || 'Unbekannt'} -

    - -
    -
    - {selectedParcel.parcel.id && ( -
    - ID: - {selectedParcel.parcel.id} -
    - )} - {selectedParcel.parcel.number && ( -
    - Nummer: - {selectedParcel.parcel.number} -
    - )} - {selectedParcel.parcel.egrid && ( -
    - EGRID: - {selectedParcel.parcel.egrid} -
    - )} - {selectedParcel.parcel.address && ( -
    - Adresse: - {selectedParcel.parcel.address} -
    - )} - {selectedParcel.parcel.area_m2 && ( -
    - Fläche (m²): - {selectedParcel.parcel.area_m2} -
    - )} -
    -
    - ))} -
    -
    - )} - - {step2Errors.parcel && ( - {step2Errors.parcel} - )} -
    - -
    - - -
    -
    - ); -}; - const CreateButton: React.FC = ({ onCreate, fields, @@ -290,31 +21,14 @@ const CreateButton: React.FC = ({ size = 'md', onSuccess, onError, - multiStep = false, ...props }) => { const { t } = useLanguage(); const [isCreating, setIsCreating] = useState(false); const [isPopupOpen, setIsPopupOpen] = useState(false); - const [currentStep, setCurrentStep] = useState<1 | 2>(1); - const [step1Data, setStep1Data] = useState({}); - const [addressData, setAddressData] = useState({ - street: '', - postalCode: '', - city: '' - }); - - // Filter fields for multi-step: Step 1 only shows "label" field - const step1Fields = useMemo(() => { - if (multiStep) { - return fields.filter(field => field.key === 'label'); - } - return fields; - }, [fields, multiStep]); // Convert CreateButtonFieldConfig to AttributeDefinition format const attributes: AttributeDefinition[] = useMemo(() => { - const fieldsToUse = multiStep ? step1Fields : fields; return fieldsToUse.map(field => { // Convert options to AttributeOption[] format let options: AttributeDefinition['options'] = undefined; @@ -371,12 +85,11 @@ const CreateButton: React.FC = ({ options: options }; }); - }, [fields, multiStep, step1Fields]); + }, [fields]); // Initialize form data with default values const initialFormData = useMemo(() => { const data: any = {}; - const fieldsToUse = multiStep ? step1Fields : fields; fieldsToUse.forEach(field => { if (field.type === 'multiselect') { // Multiselect fields should default to empty array @@ -390,116 +103,15 @@ const CreateButton: React.FC = ({ } }); return data; - }, [fields, multiStep, step1Fields]); + }, [fields]); const handleButtonClick = () => { if (!disabled && !loading && !isCreating) { setIsPopupOpen(true); - // Reset to step 1 when opening popup - if (multiStep) { - setCurrentStep(1); - setStep1Data({}); - setAddressData({ street: '', postalCode: '', city: '' }); - } - } - }; - - const handleStep1Next = (formData: any) => { - // Validate label is present - if (!formData.label || !formData.label.trim()) { - return; // FormGeneratorForm will show validation error - } - setStep1Data(formData); - setCurrentStep(2); - }; - - const handleStep2Back = () => { - setCurrentStep(1); - }; - - const handleStep2Finish = async (step2FormData: any) => { - // Combine step 1 and step 2 data - const selectedParcels = step2FormData.parzellen || []; - const completeData: any = { - label: step1Data.label, - // mandateId is NOT included - will be set by backend - }; - - // Add parzellen array if parcels were selected - include ALL parcel information for each - if (selectedParcels && selectedParcels.length > 0) { - completeData.parzellen = selectedParcels.map((selectedParcel: any) => ({ - // Basic parcel info - id: selectedParcel.parcel.id, - egrid: selectedParcel.parcel.egrid, - number: selectedParcel.parcel.number, - name: selectedParcel.parcel.name, - identnd: selectedParcel.parcel.identnd, - canton: selectedParcel.parcel.canton, - municipality_code: selectedParcel.parcel.municipality_code, - municipality_name: selectedParcel.parcel.municipality_name, - address: selectedParcel.parcel.address, - area_m2: selectedParcel.parcel.area_m2, - centroid: selectedParcel.parcel.centroid, - geoportal_url: selectedParcel.parcel.geoportal_url, - realestate_type: selectedParcel.parcel.realestate_type, - - // User-entered address fields (from step 2) - userAddress: { - street: step2FormData.address.street, - postalCode: step2FormData.address.postalCode, - city: step2FormData.address.city - }, - - // Geometry and map data - geometry: selectedParcel.map_view?.geometry_geojson, - perimeter: selectedParcel.parcel.perimeter, - map_view: selectedParcel.map_view, - - // Adjacent parcels - adjacent_parcels: selectedParcel.adjacent_parcels || [], - - // Include any other parcel properties that might exist - ...selectedParcel.parcel - })); - } - - // Send request to backend via onCreate handler - setIsCreating(true); - try { - const result = await onCreate(completeData); - - if (result?.success !== false) { - // Success - close popup - setIsPopupOpen(false); - setCurrentStep(1); - setStep1Data({}); - setAddressData({ street: '', postalCode: '', city: '' }); - - if (onSuccess) { - onSuccess(result); - } - } else { - // Handle error - if (onError) { - onError(result?.error || 'Projekt konnte nicht erstellt werden'); - } - } - } catch (error: any) { - console.error('Project creation failed:', error); - if (onError) { - onError(error.message || 'Projekt konnte nicht erstellt werden'); - } - } finally { - setIsCreating(false); } }; const handleSave = async (updatedData: any) => { - if (multiStep && currentStep === 1) { - handleStep1Next(updatedData); - return; - } - setIsCreating(true); try { @@ -508,11 +120,6 @@ const CreateButton: React.FC = ({ if (result?.success !== false) { // Success setIsPopupOpen(false); - if (multiStep) { - setCurrentStep(1); - setStep1Data({}); - setAddressData({ street: '', postalCode: '', city: '' }); - } if (onSuccess) { onSuccess(result); } @@ -534,18 +141,6 @@ const CreateButton: React.FC = ({ const handleCancel = () => { setIsPopupOpen(false); - if (multiStep) { - setCurrentStep(1); - setStep1Data({}); - setAddressData({ street: '', postalCode: '', city: '' }); - } - }; - - const handleAddressChange = (field: string, value: string) => { - setAddressData(prev => ({ - ...prev, - [field]: value - })); }; const isDisabled = disabled || loading || isCreating; @@ -590,47 +185,18 @@ const CreateButton: React.FC = ({ isOpen={isPopupOpen} title={resolvedPopupTitle} onClose={handleCancel} - size={multiStep ? 'large' : popupSize} + size={popupSize} closable={!isCreating} > - {multiStep ? ( - currentStep === 1 ? ( -
    -
    - 1 - Titel festlegen -
    - -
    - ) : ( - - - - ) - ) : ( - - )} + ); diff --git a/src/contexts/PekContext.tsx b/src/contexts/PekContext.tsx deleted file mode 100644 index fbd3e06..0000000 --- a/src/contexts/PekContext.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React, { createContext, useContext, ReactNode } from 'react'; -import { usePek } from '../hooks/usePek'; - -interface PekContextType { - // Location input - separate fields - kanton: string; - setKanton: (value: string) => void; - gemeinde: string; - setGemeinde: (value: string) => void; - adresse: string; - setAdresse: (value: string) => void; - buildLocationString: () => string; - // Legacy locationInput for backward compatibility - locationInput: string; - setLocationInput: (value: string) => void; - useCurrentLocation: () => Promise; - isGettingLocation: boolean; - locationError: string | null; - - // Parcel search - selectedParcels: any[]; - searchParcel: (location: string, includeAdjacent?: boolean) => Promise; - isSearchingParcel: boolean; - parcelSearchError: string | null; - removeParcel: (parcelId: string) => void; - clearSelectedParcels: () => void; - isParcelSelected: (parcelId: string) => boolean; - - // Map view - mapCenter: any; - mapZoomBounds: any; - parcelGeometries: any[]; - handleMapClick: (point: any) => Promise; - handleParcelClick: (parcelId: string) => Promise; - - // Command processing - commandInput: string; - setCommandInput: (value: string) => void; - processCommand: (userInput: string) => Promise; - isProcessingCommand: boolean; - commandResults: any[]; - commandError: string | null; - - // Project management - currentProjekt: any; - createProjekt: (data: any) => Promise; - isCreatingProjekt: boolean; - addParcelToProjekt: (projektId: string, data: any) => Promise; - isAddingParcel: boolean; - projektError: string | null; - - // Panel state - isPanelOpen: boolean; - setIsPanelOpen: (open: boolean) => void; -} - -const PekContext = createContext(undefined); - -export const PekProvider: React.FC<{ children: ReactNode }> = ({ children }) => { - const pekData = usePek(); - - return ( - - {children} - - ); -}; - -export const usePekContext = (): PekContextType => { - const context = useContext(PekContext); - if (!context) { - throw new Error('usePekContext must be used within a PekProvider'); - } - return context; -}; - diff --git a/src/contexts/PekTablesContext.tsx b/src/contexts/PekTablesContext.tsx deleted file mode 100644 index b4f9c59..0000000 --- a/src/contexts/PekTablesContext.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React, { createContext, useContext, ReactNode } from 'react'; -import { usePekTables } from '../hooks/usePekTables'; - -interface PekTablesContextType { - // Tables list - tables: any[]; - isLoadingTables: boolean; - tablesError: string | null; - loadTables: () => Promise; - - // Selected table - selectedTable: string; - setSelectedTable: (value: string) => void; - tableData: any[]; - isLoadingTableData: boolean; - tableDataError: string | null; - pagination: any; - loadTableData: (tableName: string, page?: number, pageSize?: number, sort?: Array<{ field: string; direction: 'asc' | 'desc' }>) => Promise; - refreshTableData: () => void; - - // Commands - commandInput: string; - setCommandInput: (value: string) => void; - processCommand: (userInput: string) => Promise; - isProcessingCommand: boolean; - commandError: string | null; - messages: any[]; - - // Create record - createRecord: (tableName: string, data: any) => Promise; -} - -const PekTablesContext = createContext(undefined); - -export const PekTablesProvider: React.FC<{ children: ReactNode }> = ({ children }) => { - const pekTablesData = usePekTables(); - - return ( - - {children} - - ); -}; - -export const usePekTablesContext = (): PekTablesContextType => { - const context = useContext(PekTablesContext); - if (!context) { - throw new Error('usePekTablesContext must be used within a PekTablesProvider'); - } - return context; -}; - diff --git a/src/core/PageManager/PageManager.tsx b/src/core/PageManager/PageManager.tsx deleted file mode 100644 index f5dc60f..0000000 --- a/src/core/PageManager/PageManager.tsx +++ /dev/null @@ -1,286 +0,0 @@ -import React, { useEffect, useState, Suspense } from 'react'; -import { useLocation } from 'react-router-dom'; -import { motion, AnimatePresence } from 'framer-motion'; -import { getPageDataByPath, GenericPageData, PageInstance } from './data'; -import PageRenderer from './PageRenderer'; -import { usePermissions } from '../../hooks/usePermissions'; - -interface PageManagerProps { - loadingComponent: React.ComponentType; - errorComponent: React.ComponentType; -} - -const PageManager: React.FC = ({ - loadingComponent: LoadingComponent, - errorComponent: ErrorComponent -}) => { - const location = useLocation(); - const [pageInstances, setPageInstances] = useState>(new Map()); - const { canView } = usePermissions(); - - // Get current path - const getCurrentPath = () => { - const path = location.pathname === '/' ? '' : location.pathname; - return path.startsWith('/') ? path.slice(1) : path; - }; - - const currentPath = getCurrentPath(); - - // Check if user has access to a page using backend RBAC permissions - const checkPageAccess = async (pageData: GenericPageData): Promise => { - console.log('🔍 PageManager: Checking page access:', { - path: pageData.path, - name: pageData.name, - hide: pageData.hide, - moduleEnabled: pageData.moduleEnabled - }); - - try { - const hasAccess = await canView('UI', pageData.path); - console.log('🔍 PageManager: Page access result:', { - path: pageData.path, - hasAccess - }); - return hasAccess; - } catch (error) { - console.error(`❌ PageManager: Error checking RBAC access for ${pageData.path}:`, error); - return false; - } - }; - - useEffect(() => { - console.log('🔄 PageManager: useEffect triggered for path:', currentPath); - const pageData = getPageDataByPath(currentPath); - - console.log('📄 PageManager: Page data found:', { - path: currentPath, - hasPageData: !!pageData, - hide: pageData?.hide, - moduleEnabled: pageData?.moduleEnabled, - name: pageData?.name - }); - - if (!pageData || pageData.hide || !pageData.moduleEnabled) { - console.log('⛔ PageManager: Page not rendered:', { - path: currentPath, - reason: !pageData ? 'not found' : pageData.hide ? 'hidden' : 'module disabled' - }); - return; - } - - // Check page access (RBAC + privilegeChecker) - console.log('🔍 PageManager: Checking access before rendering:', currentPath); - - // First check client-side privilegeChecker if provided - const checkPrivilege = async (): Promise => { - if (pageData.privilegeChecker) { - try { - const result = await pageData.privilegeChecker(); - if (!result) { - console.log('⛔ PageManager: Page blocked by privilegeChecker:', currentPath); - return false; - } - } catch (error) { - console.error('❌ PageManager: Error checking privilegeChecker:', error); - return false; - } - } - return true; - }; - - Promise.all([checkPrivilege(), checkPageAccess(pageData)]).then(([hasPrivilege, hasRBACAccess]) => { - const hasAccess = hasPrivilege && hasRBACAccess; - console.log('🔍 PageManager: Access check complete:', { - path: currentPath, - hasPrivilege, - hasRBACAccess, - hasAccess - }); - - if (!hasAccess) { - console.log('⛔ PageManager: Page not rendered due to access check:', currentPath); - return; - } - - console.log('✅ PageManager: Rendering page:', { - path: currentPath, - name: pageData.name - }); - - setPageInstances(prev => { - console.log('📦 PageManager: Creating/updating page instance:', { - path: currentPath, - existingInstances: Array.from(prev.keys()), - willCreateNew: !prev.has(currentPath) - }); - const newInstances = new Map(prev); - - // Update active states - newInstances.forEach((instance) => { - instance.isActive = instance.path === currentPath; - }); - - // Create instance if it doesn't exist - if (!newInstances.has(currentPath)) { - console.log('📦 PageManager: Creating new page instance:', { - path: currentPath, - name: pageData.name - }); - const shouldPreserve = pageData.preserveState || false; - - const pageInstance: PageInstance = { - path: currentPath, - component: ( -
    - }> - {pageData.customComponent ? ( - - ) : ( - { - }} - /> - )} - -
    - ), - isActive: true, - shouldPreserve, - pageData - }; - - newInstances.set(currentPath, pageInstance); - console.log('✅ PageManager: Page instance created:', { - path: currentPath, - totalInstances: newInstances.size, - allPaths: Array.from(newInstances.keys()) - }); - } else { - console.log('🔄 PageManager: Page instance already exists, updating active state:', currentPath); - if (import.meta.env.DEV) { - const _instance = newInstances.get(currentPath); - void _instance; // Intentionally unused, for debugging purposes - - } - } - - return newInstances; - }); - }); - - // Clean up non-preserved, inactive instances with delay for smooth transitions - const cleanupTimer = setTimeout(() => { - setPageInstances(currentInstances => { - const updatedInstances = new Map(currentInstances); - const instancesToDelete: string[] = []; - - updatedInstances.forEach((instance, path) => { - if (!instance.isActive && !instance.shouldPreserve) { - instancesToDelete.push(path); - } - }); - - instancesToDelete.forEach(path => { - - updatedInstances.delete(path); - }); - - return updatedInstances; - }); - }, 500); // Wait for transition to complete before cleanup - - return () => clearTimeout(cleanupTimer); - }, [currentPath]); - - const pageData = getPageDataByPath(currentPath); - - if (!pageData || pageData.hide || !pageData.moduleEnabled) { - return ; - } - - // Animation variants for smooth transitions - const pageVariants = { - initial: { - opacity: 0, - scale: 1, - y: 0 - }, - in: { - opacity: 1, - scale: 1, - y: 0 - }, - out: { - opacity: 0, - scale: 1, - y: 0 - } - }; - - const pageTransition = { - type: "tween" as const, - ease: "easeInOut" as const, - duration: 0.2 - }; - - return ( -
    - {Array.from(pageInstances.values()).map((instance) => { - const isVisible = instance.isActive; - - if (instance.shouldPreserve) { - // Preserved pages: Always mounted, just show/hide with animations - return ( - - {instance.component} - - ); - } else if (isVisible) { - // Non-preserved pages: Use AnimatePresence for full mount/unmount - return ( - - - {instance.component} - - - ); - } - return null; - })} -
    - ); -}; - -export default PageManager; diff --git a/src/core/PageManager/PageRenderer.tsx b/src/core/PageManager/PageRenderer.tsx deleted file mode 100644 index 12a219e..0000000 --- a/src/core/PageManager/PageRenderer.tsx +++ /dev/null @@ -1,2366 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { GenericPageData, PageButton, PageContent, resolveLanguageText, SettingsFieldConfig, SettingsSectionConfig, GenericDataHook } from './pageInterface'; -import { FormGenerator, FormGeneratorList } from '../../components/FormGenerator'; -import { FormGeneratorForm, AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm'; -import { Button, UploadButton, CreateButton, TextField, Messages, ChatMessage, LogMessage, DropdownSelect, Log, WorkflowStatus, Tabs } from '../../components/UiComponents'; -import { Popup } from '../../components/UiComponents/Popup'; -import { ConnectedFilesList } from '../../components/UiComponents/ConnectedFilesList'; -import type { DropdownSelectItem } from '../../components/UiComponents/DropdownSelect'; -import { DragDropOverlay } from '../../components/UiComponents/DragDropOverlay'; -import { useLanguage } from '../../providers/language/LanguageContext'; -import { usePermissions } from '../../hooks/usePermissions'; -import { FiPaperclip } from 'react-icons/fi'; -import { IoMdAdd } from 'react-icons/io'; -import type { WorkflowFile } from '../../hooks/playground/useDashboardInputForm'; -import styles from '../../styles/pages.module.css'; - -interface PageRendererProps { - pageData: GenericPageData; - onButtonClick?: (buttonId: string, button: PageButton) => void; -} - -// Component wrapper to fix TextField height and prevent auto-grow -const FixedHeightTextField: React.FC<{ - value: string; - onChange?: (value: string) => void; - placeholder?: string; - size?: 'sm' | 'md' | 'lg'; - disabled?: boolean; - className?: string; - onKeyDown?: (e: React.KeyboardEvent) => void; -}> = ({ value, onChange, placeholder, size, disabled, className, onKeyDown }) => { - const wrapperRef = useRef(null); - const textareaRef = useRef(null); - - useEffect(() => { - // Override the TextField's auto-grow behavior by finding the textarea and setting fixed height - const updateTextareaHeight = () => { - if (wrapperRef.current) { - // Find the textarea element - const textarea = wrapperRef.current.querySelector('textarea') as HTMLTextAreaElement; - if (textarea) { - textareaRef.current = textarea; - - // Get the input container - const inputContainer = wrapperRef.current.querySelector('.inputContainer') as HTMLElement; - - if (inputContainer) { - // Get the computed height of the input container - const containerRect = inputContainer.getBoundingClientRect(); - const containerStyle = window.getComputedStyle(inputContainer); - const paddingTop = parseFloat(containerStyle.paddingTop) || 0; - const paddingBottom = parseFloat(containerStyle.paddingBottom) || 0; - - // Get textarea border width (top + bottom) - const textareaStyle = window.getComputedStyle(textarea); - const borderTop = parseFloat(textareaStyle.borderTopWidth) || 0; - const borderBottom = parseFloat(textareaStyle.borderBottomWidth) || 0; - const borderHeight = borderTop + borderBottom; - - // Calculate available height for textarea (subtract border to prevent clipping) - const availableHeight = containerRect.height - paddingTop - paddingBottom - borderHeight; - - // Force the height - this will override TextField's auto-grow - if (availableHeight > 0) { - textarea.style.setProperty('height', `${availableHeight}px`, 'important'); - textarea.style.setProperty('min-height', `${availableHeight}px`, 'important'); - textarea.style.setProperty('max-height', `${availableHeight}px`, 'important'); - textarea.style.setProperty('overflow-y', 'auto', 'important'); - textarea.style.setProperty('resize', 'none', 'important'); - textarea.style.setProperty('box-sizing', 'border-box', 'important'); - textarea.style.setProperty('border-radius', '25px', 'important'); - // Don't set box-shadow here - it should only appear on focus via CSS - } - } else { - // Fallback: use wrapper height - const wrapperHeight = wrapperRef.current.offsetHeight; - if (wrapperHeight > 0) { - textarea.style.setProperty('height', `${wrapperHeight}px`, 'important'); - textarea.style.setProperty('min-height', `${wrapperHeight}px`, 'important'); - textarea.style.setProperty('max-height', `${wrapperHeight}px`, 'important'); - textarea.style.setProperty('overflow-y', 'auto', 'important'); - textarea.style.setProperty('resize', 'none', 'important'); - } - } - } - } - }; - - // Initial update after a short delay to ensure DOM is ready - const timeoutId = setTimeout(() => { - updateTextareaHeight(); - }, 10); - - // Use ResizeObserver to watch for container size changes - let resizeObserver: ResizeObserver | null = null; - if (wrapperRef.current && window.ResizeObserver) { - resizeObserver = new ResizeObserver(() => { - updateTextareaHeight(); - }); - resizeObserver.observe(wrapperRef.current); - } - - // Use MutationObserver to catch when TextField's useEffect runs and override it - let mutationObserver: MutationObserver | null = null; - if (wrapperRef.current) { - mutationObserver = new MutationObserver(() => { - updateTextareaHeight(); - }); - - mutationObserver.observe(wrapperRef.current, { - attributes: true, - childList: true, - subtree: true, - attributeFilter: ['style', 'class'] - }); - } - - // Use an interval as a fallback to continuously override auto-grow - const interval = setInterval(updateTextareaHeight, 100); - - // Also listen for resize events - window.addEventListener('resize', updateTextareaHeight); - - return () => { - clearTimeout(timeoutId); - if (resizeObserver) { - resizeObserver.disconnect(); - } - if (mutationObserver) { - mutationObserver.disconnect(); - } - clearInterval(interval); - window.removeEventListener('resize', updateTextareaHeight); - }; - }, [value]); - - return ( -
    - -
    - ); -}; - -// Component for rendering tabs from page content -const TabsRenderer: React.FC<{ - content: PageContent; - renderContent: (content: PageContent, key?: string | number) => React.ReactNode; - t: (key: string, fallback?: string) => string; -}> = ({ content, renderContent, t }) => { - const tabsConfig = content.tabsConfig; - if (!tabsConfig || !tabsConfig.tabs || tabsConfig.tabs.length === 0) { - return null; - } - - // Convert PageContent tabs to Tabs component format - const tabs = tabsConfig.tabs.map(tab => ({ - id: tab.id, - label: resolveLanguageText(tab.label, t), - content: ( - <> - {tab.content.map((nestedContent, index) => ( - - {renderContent(nestedContent)} - - ))} - - ) - })); - - return ( - - ); -}; - -// Component to handle async permission checks for content -const ContentRenderer: React.FC<{ - contents: PageContent[]; - renderContent: (content: PageContent) => React.ReactNode; - hasPermission: (context: 'DATA' | 'UI' | 'RESOURCE', item: string, operation?: 'read' | 'create' | 'update' | 'delete') => Promise; -}> = ({ contents, renderContent, hasPermission }) => { - const [visibleContents, setVisibleContents] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - const checkPermissions = async () => { - setLoading(true); - const visible: PageContent[] = []; - - for (const content of contents) { - const contentId = content.id || `content-${content.type}`; - let shouldRender = true; - - // Check RBAC permissions for content - try { - const hasRBACAccess = await hasPermission('UI', contentId, 'read'); - if (!hasRBACAccess) { - shouldRender = false; - } - } catch (error) { - console.error(`Error checking RBAC access for content ${contentId}:`, error); - shouldRender = false; - } - - if (shouldRender) { - visible.push(content); - } - } - - setVisibleContents(visible); - setLoading(false); - }; - - checkPermissions(); - }, [contents, hasPermission]); - - if (loading) { - return null; // Or return a loading indicator if desired - } - - // Check if this is a chatbot layout pattern: chatHistory, messages, inputForm - const hasChatHistory = visibleContents.some(c => c.type === 'chatHistory'); - const hasMessages = visibleContents.some(c => c.type === 'messages'); - const hasInputForm = visibleContents.some(c => c.type === 'inputForm'); - const isChatbotLayout = hasChatHistory && hasMessages && hasInputForm; - - // Check if this is a dashboard layout pattern: messages, log, inputForm - const hasLog = visibleContents.some(c => c.type === 'log'); - const isDashboardLayout = hasMessages && hasLog && hasInputForm && !isChatbotLayout; - - if (isChatbotLayout) { - // Render chatbot two-column layout - const chatHistoryContent = visibleContents.find(c => c.type === 'chatHistory'); - const messagesContent = visibleContents.find(c => c.type === 'messages'); - const inputFormContent = visibleContents.find(c => c.type === 'inputForm'); - const otherContents = visibleContents.filter(c => - c.type !== 'chatHistory' && c.type !== 'messages' && c.type !== 'inputForm' - ); - - return ( - <> -
    - {/* Left column: Chat History */} - {chatHistoryContent && ( -
    - {renderContent(chatHistoryContent)} -
    - )} - {/* Right column: Messages and Input Form */} -
    - {messagesContent && ( -
    - {renderContent(messagesContent)} -
    - )} - {inputFormContent && ( -
    - {renderContent(inputFormContent)} -
    - )} -
    -
    - {/* Render any other content sections */} - {otherContents.map((content, index) => ( - - {renderContent(content)} - - ))} - - ); - } - - if (isDashboardLayout) { - // Render dashboard grid layout - const messagesContent = visibleContents.find(c => c.type === 'messages'); - const logContent = visibleContents.find(c => c.type === 'log'); - const inputFormContent = visibleContents.find(c => c.type === 'inputForm'); - const otherContents = visibleContents.filter(c => - c.type !== 'messages' && c.type !== 'log' && c.type !== 'inputForm' - ); - - return ( - <> -
    - {/* Top row: Messages | Log */} -
    - {messagesContent && ( -
    - {renderContent(messagesContent)} -
    - )} - {logContent && ( -
    - {renderContent(logContent)} -
    - )} -
    - {/* Bottom row: Input Form (which includes connected files) */} - {inputFormContent && ( -
    - {renderContent(inputFormContent)} -
    - )} -
    - {/* Render any other content sections */} - {otherContents.map((content, index) => ( - - {renderContent(content)} - - ))} - - ); - } - - return ( - <> - {visibleContents.map((content, index) => ( - - {renderContent(content)} - - ))} - - ); -}; - -// Helper function to recursively find all table content sections -const findAllTableContents = (contents: PageContent[]): PageContent[] => { - const tableContents: PageContent[] = []; - - const traverse = (contentList: PageContent[]) => { - for (const content of contentList) { - if (content.type === 'table') { - tableContents.push(content); - } - // Check nested content in tabs - if (content.type === 'tabs' && content.tabsConfig) { - for (const tab of content.tabsConfig.tabs) { - traverse(tab.content); - } - } - // Check nested content in columns - if (content.type === 'columns' && content.columnsConfig) { - for (const column of content.columnsConfig.columns) { - traverse(column.content); - } - } - } - }; - - traverse(contents); - return tableContents; -}; - -const PageRenderer: React.FC = ({ - pageData, - onButtonClick -}) => { - // Get translation function from language context - const { t } = useLanguage(); - const { hasPermission } = usePermissions(); - - // Find all table content sections (including nested ones) - const allTableContents = React.useMemo(() => - findAllTableContents(pageData.content || []), - [pageData.content] - ); - - // Create hook instances for all table contents - MUST be at top level - // We need to call hookFactory() at top level, but the actual hook() calls happen below - const tableHookFactories = React.useMemo(() => { - return allTableContents.map((tableContent, index) => { - const hookFactory = tableContent.tableConfig?.hookFactory; - if (hookFactory) { - const key = tableContent.id || `table-${index}`; - return { key, hookFactory }; - } - return null; - }).filter((item): item is { key: string; hookFactory: () => () => GenericDataHook } => item !== null); - }, [allTableContents]); - - // Call all hook factories at top level to create hook instances - // This must happen unconditionally and in the same order every render - const tableHookInstances = tableHookFactories.map(({ key, hookFactory }) => ({ - key, - hook: hookFactory() // This creates the hook function, doesn't call it yet - })); - - // Call all hooks at top level - MUST be unconditional - // All hooks are called in the same order every render - const tableHookDataArray = tableHookInstances.map(({ key, hook }) => ({ - key, - data: hook() // This is the actual hook call - must be at top level - })); - - // Convert to Map for easy lookup - const tableHookData = React.useMemo(() => { - const dataMap = new Map(); - tableHookDataArray.forEach(({ key, data }) => { - dataMap.set(key, data); - }); - return dataMap; - }, [tableHookDataArray]); - - // Also check for top-level inputForm and settings (for backward compatibility) - const inputFormContent = pageData.content?.find(content => content.type === 'inputForm'); - const settingsContent = pageData.content?.find(content => content.type === 'settings'); - const hookFactory = inputFormContent?.inputFormConfig?.hookFactory - || settingsContent?.settingsConfig?.hookFactory; - - // Create hook instance at top level - const useTableData = hookFactory ? hookFactory() : null; - - // Call the hook to get the current data (for backward compatibility) - // If no inputForm/settings hook, try to use the first table hook for header buttons - let hookData = useTableData ? useTableData() : null; - if (!hookData && tableHookData.size > 0) { - // Use the first table hook data for header buttons - const firstTableHook = Array.from(tableHookData.values())[0]; - hookData = firstTableHook; - } - - - - // Handle button clicks - const handleButtonClick = async (button: PageButton) => { - try { - // Check RBAC permissions - // Determine operation based on button type/action - let operation: 'read' | 'create' | 'update' | 'delete' | undefined = undefined; - if (button.id.includes('delete') || button.id.includes('remove')) { - operation = 'delete'; - } else if (button.id.includes('create') || button.id.includes('add') || button.id.includes('new')) { - operation = 'create'; - } else if (button.id.includes('edit') || button.id.includes('update') || button.id.includes('save')) { - operation = 'update'; - } else { - operation = 'read'; - } - - try { - const hasRBACAccess = await hasPermission('UI', button.id, operation); - if (!hasRBACAccess) { - return; - } - } catch (error) { - console.error(`Error checking RBAC access for button ${button.id}:`, error); - return; - } - - // Call the button's onClick handler with hook data - if (button.onClick) { - await button.onClick(hookData); - } - - // Call the parent handler - if (onButtonClick) { - onButtonClick(button.id, button); - } - } catch (error) { - console.error(`Error handling button click for ${button.id}:`, error); - } - }; - - // Helper function to get nested value using dot notation (generic utility) - const getNestedValue = (obj: any, path: string): any => { - return path.split('.').reduce((current, key) => current?.[key], obj); - }; - - // Helper function to set nested value using dot notation (generic utility) - const setNestedValue = (obj: any, path: string, value: any): any => { - const keys = path.split('.'); - const result = { ...obj }; - let current = result; - for (let i = 0; i < keys.length - 1; i++) { - const key = keys[i]; - if (!(key in current)) { - current[key] = {}; - } - current[key] = { ...current[key] }; - current = current[key]; - } - current[keys[keys.length - 1]] = value; - return result; - }; - - // Generic form section renderer - reusable for any form-based content - const FormSectionRenderer: React.FC<{ - sections: SettingsSectionConfig[]; - formData: any; - fieldsBySection: Record; - loadingBySection: Record; - errorsBySection: Record; - onSave?: (sectionId: string, data: any) => Promise; - getNestedValue: (obj: any, path: string) => any; - setNestedValue: (obj: any, path: string, value: any) => any; - }> = ({ sections, formData, fieldsBySection, loadingBySection, errorsBySection, onSave, getNestedValue, setNestedValue: _setNestedValue }) => { - const [sectionFormData, setSectionFormData] = useState>({}); - const [sectionSaveLoading, setSectionSaveLoading] = useState>({}); - const [sectionSaveMessages, setSectionSaveMessages] = useState>({}); - const formRefs = useRef>({}); - - // Initialize form data from formData when it changes - useEffect(() => { - const newFormData: Record = {}; - sections.forEach(section => { - const allFields = [ - ...(section.staticFields || []), - ...(fieldsBySection[section.sectionId] || []) - ]; - - if (allFields.length === 0) return; - - const sectionData: any = { ...(sectionFormData[section.id] || {}) }; - allFields.forEach(field => { - const value = getNestedValue(formData, field.dataKey); - if (value !== undefined && sectionData[field.dataKey] !== value) { - sectionData[field.dataKey] = value; - } - }); - newFormData[section.id] = sectionData; - }); - - const hasChanges = Object.keys(newFormData).some(sectionId => { - const newData = newFormData[sectionId]; - const oldData = sectionFormData[sectionId] || {}; - return JSON.stringify(newData) !== JSON.stringify(oldData); - }); - - if (hasChanges) { - setSectionFormData(prev => ({ ...prev, ...newFormData })); - } - }, [formData, sections.length, JSON.stringify(fieldsBySection)]); - - // Helper function to convert SettingsFieldConfig to AttributeDefinition - const convertFieldToAttribute = (field: SettingsFieldConfig): AttributeDefinition => { - // Determine the type based on field.type and inputType - let attributeType: AttributeDefinition['type'] = 'text'; - if (field.type === 'select') { - attributeType = 'select'; - } else if (field.type === 'toggle') { - attributeType = 'boolean'; - } else if (field.type === 'text' && field.inputType) { - // Map inputType to attribute type - if (field.inputType === 'email') { - attributeType = 'email'; - } else if (field.inputType === 'tel') { - attributeType = 'text'; // tel is not a separate type in AttributeDefinition - } else { - attributeType = 'text'; - } - } - - // Convert options format if present - let options: AttributeDefinition['options'] = undefined; - if (field.options && field.options.length > 0) { - options = field.options.map(opt => ({ - value: opt.value, - label: typeof opt.label === 'string' ? opt.label : resolveLanguageText(opt.label, t) - })); - } - - // Determine if field is disabled/readonly - const isDisabled = typeof field.disabled === 'function' - ? field.disabled(formData) - : field.disabled || false; - - return { - name: field.dataKey, - type: attributeType, - label: typeof field.label === 'string' ? field.label : resolveLanguageText(field.label, t), - description: field.description ? (typeof field.description === 'string' ? field.description : resolveLanguageText(field.description, t)) : undefined, - required: field.required || false, - readonly: isDisabled, - editable: !isDisabled, - placeholder: field.placeholder ? (typeof field.placeholder === 'string' ? field.placeholder : resolveLanguageText(field.placeholder, t)) : undefined, - options: options - }; - }; - - const handleSectionSave = async (section: typeof sections[0], formDataToSave?: any) => { - // If formDataToSave is provided (from FormGeneratorForm onSubmit), use it - // Otherwise, try to get it from the form ref or use current sectionFormData - const dataToSave = formDataToSave || sectionFormData[section.id] || {}; - - setSectionSaveLoading(prev => ({ ...prev, [section.id]: true })); - setSectionSaveMessages(prev => ({ ...prev, [section.id]: null })); - - try { - if (onSave) { - await onSave(section.id, dataToSave); - } else if (section.onSave) { - await section.onSave(section.id, dataToSave); - } - - setSectionSaveMessages(prev => ({ - ...prev, - [section.id]: { - type: 'success', - text: t('settings.save_success') || 'Settings saved successfully' - } - })); - - setTimeout(() => { - setSectionSaveMessages(prev => ({ ...prev, [section.id]: null })); - }, 3000); - } catch (error: any) { - setSectionSaveMessages(prev => ({ - ...prev, - [section.id]: { - type: 'error', - text: error.message || t('settings.save_error') || 'Failed to save settings' - } - })); - } finally { - setSectionSaveLoading(prev => ({ ...prev, [section.id]: false })); - } - }; - - return ( -
    - {sections.map(section => { - // Check if section has conditional rendering logic - if (section.renderCondition) { - const shouldRender = section.renderCondition(formData); - if (!shouldRender) { - // Render alternative content if provided - if (section.renderAlternative) { - return ( - - {section.renderAlternative(formData, t, resolveLanguageText)} - - ); - } - return null; - } - } - - const allFields = [ - ...(section.staticFields || []), - ...(fieldsBySection[section.sectionId] || []) - ]; - const isLoading = loadingBySection[section.sectionId] || false; - const error = errorsBySection[section.sectionId]; - const saveLoading = sectionSaveLoading[section.id] || false; - const saveMessage = sectionSaveMessages[section.id]; - - // Convert fields to AttributeDefinition format - const attributes: AttributeDefinition[] = allFields.map(convertFieldToAttribute); - - // Get current form data for this section - const currentSectionFormData = sectionFormData[section.id] || {}; - // Prepare section form data (flatten nested structure for FormGeneratorForm) - const formDataForSection: Record = {}; - allFields.forEach(field => { - const value = currentSectionFormData[field.dataKey] !== undefined - ? currentSectionFormData[field.dataKey] - : getNestedValue(formData, field.dataKey); - if (value !== undefined) { - formDataForSection[field.dataKey] = value; - } - }); - - return ( -
    - {/* Section Header */} -
    - {section.icon && ( - - )} -

    - {resolveLanguageText(section.title, t)} -

    - {section.description && ( -

    - {resolveLanguageText(section.description, t)} -

    - )} -
    - - {/* Loading State */} - {isLoading && ( -
    - {t('common.loading')} -
    - )} - - {/* Error State */} - {error && !isLoading && ( -
    - {error} -
    - )} - - {/* FormGeneratorForm */} - {!isLoading && !error && attributes.length > 0 && ( - <> -
    { - if (el) { - const form = el.querySelector('form'); - if (form) { - formRefs.current[section.id] = form; - } - } - }}> - { - // Update local section form data - setSectionFormData(prev => ({ - ...prev, - [section.id]: { ...prev[section.id], ...formDataToSave } - })); - // Trigger save with form data - await handleSectionSave(section, formDataToSave); - }} - showButtons={false} - className="" - /> -
    - - {/* Save Message */} - {saveMessage && ( -
    - {saveMessage.text} -
    - )} - - {/* Save Button */} -
    - -
    - - )} -
    - ); - })} -
    - ); - }; - - // Render content based on type - const renderContent = (content: PageContent) => { - // Wrapper functions to convert fileId-based handlers to WorkflowFile-based handlers - // These are defined at the top level of renderContent so they're accessible in all content cases - const wrapFileDelete: ((file: WorkflowFile) => Promise) | undefined = hookData?.handleFileDelete ? async (file: WorkflowFile) => { - if (!hookData?.handleFileDelete || !file) return; - const handler = hookData.handleFileDelete as any; - // Check if handler expects fileId (string) or file (WorkflowFile) - if (file?.fileId && typeof file.fileId === 'string') { - // Try fileId signature first (handler might be (fileId: string, ...) => Promise) - try { - const result = handler(file.fileId); - if (result instanceof Promise) await result; - return; - } catch { - // Fall through to file signature - } - } - // Try file signature (handler might be (file: WorkflowFile) => Promise) - const result = handler(file); - if (result instanceof Promise) await result; - } : undefined; - - const wrapFileRemove: ((file: WorkflowFile) => Promise) | undefined = hookData?.handleFileRemove ? async (file: WorkflowFile) => { - if (!hookData?.handleFileRemove || !file) return; - const handler = hookData.handleFileRemove as any; - // Check if handler expects fileId (string) or file (WorkflowFile) - if (file?.fileId && typeof file.fileId === 'string') { - // Try fileId signature first (handler might be (fileId: string) => void | Promise) - try { - const result = handler(file.fileId); - if (result instanceof Promise) await result; - return; - } catch { - // Fall through to file signature - } - } - // Try file signature (handler might be (file: WorkflowFile) => Promise) - const result = handler(file); - if (result instanceof Promise) await result; - } : undefined; - - switch (content.type) { - case 'heading': - const HeadingTag = `h${content.level || 2}` as keyof React.JSX.IntrinsicElements; - return React.createElement( - HeadingTag, - { key: content.id, className: styles.contentHeading }, - resolveLanguageText(content.content, t) - ); - - case 'paragraph': - return ( -

    - {resolveLanguageText(content.content, t)} -

    - ); - - case 'list': - return ( -
    - {content.content && ( -

    {resolveLanguageText(content.content, t)}

    - )} -
      - {content.items?.map((item, index) => ( -
    • - {resolveLanguageText(item, t)} -
    • - ))} -
    -
    - ); - - case 'code': - return ( -
    -                        
    -                            {resolveLanguageText(content.content, t)}
    -                        
    -                    
    - ); - - case 'divider': - return
    ; - - case 'custom': - if (content.customComponent) { - const CustomComponent = content.customComponent; - return ; - } - return null; - - case 'table': - // Get hookData for this specific table (nested tables use their own hooks) - const currentTableHookData = content.id && tableHookData.has(content.id) - ? tableHookData.get(content.id)! - : hookData; // Fallback to top-level hookData for backward compatibility - - if (content.tableConfig && currentTableHookData) { - - const { columns: configColumns, actionButtons, customActions, emptyMessage, ...tableProps } = content.tableConfig; - - // Only show loading spinner on initial load (when there's no data yet) - // During refetch, keep the existing data visible - const showLoadingSpinner = currentTableHookData.loading && currentTableHookData.data.length === 0; - - // Show error state if there's an error - if (currentTableHookData.error) { - return ( -
    -
    -

    Error loading data: {currentTableHookData.error}

    - {currentTableHookData.refetch && ( - - )} -
    -
    - ); - } - - // Use columns from hook data if available, otherwise use config columns - // CRITICAL: Preserve columns even when data is empty (e.g., after filtering) - // Columns from attributes should persist regardless of data state - const hookColumns = currentTableHookData.columns && currentTableHookData.columns.length > 0 ? currentTableHookData.columns : undefined; - const configCols = configColumns && configColumns.length > 0 ? configColumns : undefined; - // Prioritize hookColumns (from attributes) over configColumns to ensure persistence - const columns = hookColumns || configCols; - - // CRITICAL: Resolve LanguageText objects in column labels - // Only map if columns exist, otherwise FormGenerator will auto-detect - const resolvedColumns = columns ? columns.map(col => ({ - ...col, - label: resolveLanguageText(col.label, t) - })) : undefined; - - // Convert action buttons to FormGenerator format - // Filter out buttons that should be hidden based on RBAC permissions - const formGeneratorActions = actionButtons?.filter(action => { - // Check if button should be hidden based on permissions - const permissions = (hookData as any)?.permissions; - if (!permissions) { - // If no permissions loaded yet, show button (will be filtered later) - return true; - } - - // Determine which permission to check based on button type - // Only standard action types: edit, delete, view, copy - let requiredPermission: 'read' | 'create' | 'update' | 'delete' | null = null; - if (action.type === 'view') { - requiredPermission = 'read'; - } else if (action.type === 'edit') { - requiredPermission = 'update'; - } else if (action.type === 'copy') { - // Copy creates a new item, so it requires 'create' permission - requiredPermission = 'create'; - } else if (action.type === 'delete') { - requiredPermission = 'delete'; - } - - // If no specific permission required, show button - if (!requiredPermission) { - return true; - } - - // Check if user has the required permission (not 'n') - const hasPermission = permissions[requiredPermission] !== 'n' && permissions.view; - - // Log permission check for debugging - if (import.meta.env.DEV) { - console.log(`🔐 Permission check for ${action.type} button:`, { - requiredPermission, - hasPermission, - permissionValue: permissions[requiredPermission], - view: permissions.view - }); - } - - return hasPermission; - }).map(action => { - // Wrap disabled function to handle both row-based and hookData-based disabled functions - let disabledFn: ((row: any) => boolean | { disabled: boolean; message?: string }); - if (action.disabled) { - if (typeof action.disabled === 'function') { - // Try to call with hookData first (for permission-based checks) - // If that works, use the result for all rows - // Otherwise, fall back to calling with row - try { - // Check if function signature suggests it takes hookData - // We'll try calling it with hookData - if it's designed for that, it will work - const testCall = (action.disabled as any)(hookData); - if (testCall !== undefined && (typeof testCall === 'boolean' || (typeof testCall === 'object' && 'disabled' in testCall))) { - // Function accepts hookData - use result for all rows - disabledFn = () => testCall; - } else { - // Function doesn't work with hookData, use row-based approach - disabledFn = action.disabled as (row: any) => boolean | { disabled: boolean; message?: string }; - } - } catch { - // Function doesn't accept hookData, use row-based approach - disabledFn = action.disabled as (row: any) => boolean | { disabled: boolean; message?: string }; - } - } else { - // Non-function disabled value - const disabledValue = action.disabled; - if (typeof disabledValue === 'boolean') { - disabledFn = () => disabledValue; - } else if (disabledValue && typeof disabledValue === 'object' && 'disabled' in disabledValue) { - disabledFn = () => disabledValue as { disabled: boolean; message?: string }; - } else { - disabledFn = () => false; - } - } - } else { - disabledFn = () => false; - } - - return { - type: action.type, - onAction: action.onAction, - // CRITICAL: Resolve LanguageText objects in action titles - title: resolveLanguageText(action.title, t), - isProcessing: action.loading || (() => false), - disabled: disabledFn, - // Preserve field mappings and operation names - idField: action.idField, - nameField: action.nameField, - typeField: action.typeField, - contentField: action.contentField, - operationName: action.operationName, - loadingStateName: action.loadingStateName, - fetchItemFunctionName: action.fetchItemFunctionName - }; - }) || []; - - // Debug logging for table rendering - if (import.meta.env.DEV) { - console.log('🔍 Rendering FormGenerator:', { - dataLength: currentTableHookData.data?.length || 0, - columnsCount: resolvedColumns?.length || 0, - loading: showLoadingSpinner, - hasError: !!currentTableHookData.error, - data: currentTableHookData.data, - willAutoDetect: !resolvedColumns - }); - } - - return ( -
    - {currentTableHookData.isRefetching && ( -
    - Refreshing... -
    - )} - { - // Resolve LanguageText in title to string - let resolvedTitle: string | ((row: any) => string) | undefined = undefined; - if (typeof action.title === 'function') { - resolvedTitle = action.title; - } else if (typeof action.title === 'string') { - resolvedTitle = resolveLanguageText(action.title, t); - } else if (action.title && typeof action.title === 'object') { - resolvedTitle = resolveLanguageText(action.title as any, t); - } - return { - ...action, - title: resolvedTitle - }; - })} - hookData={currentTableHookData} - onDelete={currentTableHookData.onDelete} - onDeleteMultiple={currentTableHookData.onDeleteMultiple} - emptyMessage={emptyMessage} - {...tableProps} - /> -
    - ); - } - return null; - - case 'inputForm': - if (content.inputFormConfig && hookData) { - const config = content.inputFormConfig; - const isRunning = hookData.isRunning || false; - - // Determine button props based on workflow state - const buttonLabel = isRunning - ? (config.stopButtonLabel || config.buttonLabel) - : config.buttonLabel; - const buttonIcon = isRunning - ? (config.stopButtonIcon || config.buttonIcon) - : config.buttonIcon; - const buttonVariant = isRunning - ? (config.stopButtonVariant || config.buttonVariant || 'primary') - : (config.buttonVariant || 'primary'); - // Button disabled logic: - // - Always enabled when running (to allow stopping), unless submitting - // - When not running, disabled if submitting or input is empty - const buttonDisabled = isRunning - ? hookData.isSubmitting // When running, only disable if submitting - : (hookData.isSubmitting || !hookData.inputValue?.trim()); // When not running, disable if submitting or input empty - - // Handle Enter key press - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey && hookData.handleSubmit && !hookData.isSubmitting) { - e.preventDefault(); - hookData.handleSubmit(); - } - }; - - // Check if we have file management (dashboard style with workflowFiles) - const hasFileManagement = !!(hookData.handleFileUpload && hookData.workflowFiles !== undefined); - - // Check if we have chatbot file upload (simpler style with uploadedFiles) - // Also check if file upload is enabled in config (default: true) - const showFileUpload = config.showFileUpload !== false; // Default to true if not specified - const hasChatbotFileUpload = showFileUpload && !!(hookData.handleFileUpload && hookData.uploadedFiles !== undefined); - - // Check RBAC permissions for prompt selector and workflow mode selector - // Show prompt selector if user has permission to view/read prompts (even if no prompts exist yet) - const showPromptSelector = hookData.promptPermission && - hookData.promptPermission.view !== false && - hookData.promptPermission.read !== 'n'; - const showWorkflowModeSelector = hookData.workflowModeItems !== undefined; - - // Calculate number of visible elements for equal width distribution - const visibleElementsCount = - (showPromptSelector ? 1 : 0) + - (showWorkflowModeSelector ? 1 : 0) + - 2; // Attach Files and Send buttons are always visible - - // Grid layout for pages with file management - if (hasFileManagement) { - return ( -
    - {/* Top row: Dropdown selectors and buttons */} -
    - {showPromptSelector && ( -
    - {})} - placeholder={t('dashboard.prompt.select', 'Select a prompt')} - emptyMessage={t('dashboard.prompt.empty', 'No prompts available')} - headerText={t('dashboard.prompt.header', 'Select Prompt')} - variant="secondary" - size={config.buttonSize || 'md'} - disabled={hookData.isSubmitting || hookData.promptsLoading || false} - loading={hookData.promptsLoading || false} - minWidth="0" - /> -
    - )} - {showWorkflowModeSelector && ( -
    - {})} - placeholder={t('dashboard.workflow.mode.select', 'Select workflow mode')} - emptyMessage={t('dashboard.workflow.mode.empty', 'No modes available')} - headerText={t('dashboard.workflow.mode.header', 'Workflow Mode')} - variant="secondary" - size={config.buttonSize || 'md'} - disabled={hookData.isSubmitting || false} - showClearButton={true} - minWidth="0" - /> -
    - )} -
    - -
    -
    - -
    -
    - - {/* Bottom row: Input text area */} -
    -
    - -
    -
    - - {/* Right column: Connected Files List (spans both rows) */} -
    -
    - { - const result = hookData.handleFileAttach!(fileId); - if (result instanceof Promise) await result; - } : undefined} - deletingFiles={hookData.deletingFiles || new Set()} - previewingFiles={hookData.previewingFiles || new Set()} - removingFiles={new Set()} // Can be tracked if needed - workflowId={hookData.workflowId} - emptyMessage="No files connected to this workflow" - /> -
    -
    - - {/* File Attachment Popup */} - {hookData.isFileAttachmentPopupOpen && ( - hookData.setIsFileAttachmentPopupOpen?.(false)} - size="large" - > -
    - {/* Upload Button Section */} -
    - { - const handler = hookData.handleFileUploadAndAttach || hookData.handleFileUpload; - if (handler) { - // Handler returns Promise<{ success, data }>, but UploadButton expects Promise - await handler(file); - } - } : async () => {}} - disabled={hookData.isSubmitting || false} - loading={hookData.uploadingFile || false} - variant="primary" - size="md" - multiple={true} - > - Upload New File - -
    - - {/* Files List */} -
    - {hookData.allUserFiles && hookData.allUserFiles.length > 0 ? ( -
    - {hookData.allUserFiles.map((file: any) => { - const isAttached = hookData.pendingFiles?.some((pf: any) => pf.fileId === file.id); - return ( -
    hookData.handleFileAttach?.(file.id)} - style={{ - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - padding: '0.75rem', - border: `1px solid ${isAttached ? '#4CAF50' : '#e0e0e0'}`, - borderRadius: '4px', - cursor: 'pointer', - backgroundColor: isAttached ? '#f1f8f4' : 'transparent', - transition: 'all 0.2s' - }} - onMouseEnter={(e) => { - if (!isAttached) { - e.currentTarget.style.backgroundColor = '#f5f5f5'; - } - }} - onMouseLeave={(e) => { - if (!isAttached) { - e.currentTarget.style.backgroundColor = 'transparent'; - } - }} - > -
    - 📎 -
    -
    - {file.file_name || file.fileName || 'Unknown File'} -
    -
    - {(() => { - const size = file.size; - const mimeType = file.mime_type || file.mimeType || 'application/octet-stream'; - let sizeStr = ''; - if (size) { - if (size < 1024) { - sizeStr = `${size} B`; - } else if (size < 1024 * 1024) { - sizeStr = `${(size / 1024).toFixed(1)} KB`; - } else if (size < 1024 * 1024 * 1024) { - sizeStr = `${(size / (1024 * 1024)).toFixed(1)} MB`; - } else { - sizeStr = `${(size / (1024 * 1024 * 1024)).toFixed(1)} GB`; - } - } - return sizeStr ? `${sizeStr} • ${mimeType}` : mimeType; - })()} -
    -
    -
    -
    - {isAttached ? 'Attached' : 'Attach'} -
    -
    - ); - })} -
    - ) : ( -
    - No files uploaded yet. Click "Upload New File" to add files. -
    - )} -
    -
    -
    - )} -
    - ); - } - - // Chatbot file upload layout (simpler than dashboard) - if (hasChatbotFileUpload) { - const uploadedFiles = hookData.uploadedFiles || []; - return ( -
    - {/* Input and buttons row */} -
    -
    - -
    -
    - { - await hookData.handleFileUpload!(file); - // Error handling is done in the hook - } : async () => {}} - disabled={hookData.isSubmitting || false} - loading={hookData.uploadingFile || false} - variant="secondary" - size={config.buttonSize || 'md'} - multiple={true} - accept="*/*" - > - Upload - - -
    -
    - - {/* Pending files display */} - {uploadedFiles.length > 0 && ( -
    - {uploadedFiles.map((file: { fileId: string; fileName: string }) => ( -
    - 📎 - - {file.fileName} - - -
    - ))} -
    - )} - - {/* Upload error display */} - {hookData.uploadError && ( -
    - {hookData.uploadError} -
    - )} -
    - ); - } - - // Default layout without files (backward compatible) - return ( -
    - {/* Dropdown selectors row */} - {(showPromptSelector || showWorkflowModeSelector) && ( -
    - {showPromptSelector && ( - {})} - placeholder={t('dashboard.prompt.select', 'Select a prompt')} - emptyMessage={t('dashboard.prompt.empty', 'No prompts available')} - headerText={t('dashboard.prompt.header', 'Select Prompt')} - variant="secondary" - size={config.buttonSize || 'md'} - disabled={hookData.isSubmitting || hookData.promptsLoading || false} - loading={hookData.promptsLoading || false} - minWidth="200px" - /> - )} - {showWorkflowModeSelector && ( - {})} - placeholder={t('dashboard.workflow.mode.select', 'Select workflow mode')} - emptyMessage={t('dashboard.workflow.mode.empty', 'No modes available')} - headerText={t('dashboard.workflow.mode.header', 'Workflow Mode')} - variant="secondary" - size={config.buttonSize || 'md'} - disabled={hookData.isSubmitting || false} - minWidth="180px" - showClearButton={true} - /> - )} -
    - )} - - {/* Input and button row */} -
    -
    - -
    -
    - -
    -
    -
    - ); - } - return null; - - case 'messages': - const config = content.messagesConfig || {}; - const dataSource = config.dataSource || 'messages'; - - const hookMessages = Array.isArray(hookData?.messages) ? hookData.messages : []; - const hookLogs = Array.isArray(hookData?.logs) ? hookData.logs : []; - - // Determine which data to render based on dataSource - let messagesToRender: any[] = []; - let variantToUse = config.variant || 'chat'; - - if (dataSource === 'logs') { - messagesToRender = hookLogs; - // Logs should use log variant by default - variantToUse = config.variant || 'log'; - } else if (dataSource === 'both') { - // Merge messages and logs, with logs using log variant - // We'll render them separately in sequence - const combined = [ - ...hookMessages.map((msg: any) => ({ ...msg, _renderVariant: config.variant || 'chat' })), - ...hookLogs.map((log: any) => ({ ...log, _renderVariant: 'log' })) - ].sort((a, b) => { - // Sort by timestamp/sequence to interleave properly - const aTime = a.publishedAt || a.timestamp || 0; - const bTime = b.publishedAt || b.timestamp || 0; - return aTime - bTime; - }); - messagesToRender = combined; - // When both, we'll use a custom renderer - } else { - // Default: use messages - messagesToRender = hookMessages; - } - - // Custom renderer for when dataSource is 'both' to handle different variants - const renderMessage = dataSource === 'both' - ? (message: any, index: number) => { - const variant = message._renderVariant || 'chat'; - const cleanMessage = { ...message }; - delete cleanMessage._renderVariant; - - if (variant === 'log') { - return ( - - ); - } else { - return ( - - ); - } - } - : undefined; - - return ( -
    - -
    - ); - - case 'settings': - if (content.settingsConfig && hookData) { - const config = content.settingsConfig; - const sections = config.sections; - const formData = (hookData as any).settingsData || {}; - const fieldsBySection = (hookData as any).settingsFields || {}; - const loadingBySection = (hookData as any).settingsLoading || {}; - const errorsBySection = (hookData as any).settingsErrors || {}; - const saveSectionHandler = (hookData as any).saveSection; - - // Use a generic form renderer component that can be reused - return ; - } - return null; - - case 'log': { - const logConfig = content.logConfig || {}; - const dashboardTree = hookData?.dashboardTree; - const onToggleOperationExpanded = hookData?.onToggleOperationExpanded; - const getChildOperations = hookData?.getChildOperations; - return ( -
    - -
    - ); - } - - case 'tabs': { - return ( - - ); - } - - case 'columns': { - const columnsConfig = content.columnsConfig; - if (!columnsConfig || !columnsConfig.columns || columnsConfig.columns.length === 0) { - return null; - } - - // Build grid template columns - const gridTemplateColumns = columnsConfig.columns - .map(col => col.width || '1fr') - .join(' '); - const gap = columnsConfig.gap || '1rem'; - - return ( -
    - {columnsConfig.columns.map((column, colIndex) => ( -
    - {column.content.map((nestedContent, index) => ( - - {renderContent(nestedContent)} - - ))} -
    - ))} -
    - ); - } - - case 'chatHistory': { - const config = content.chatHistoryConfig || {}; - const threads = (hookData as any)?.threads || []; - const selectedThreadId = (hookData as any)?.selectedThreadId || null; - const threadsLoading = (hookData as any)?.threadsLoading || false; - const threadsError = (hookData as any)?.threadsError || null; - const selectThread = (hookData as any)?.selectThread; - const handleDelete = (hookData as any)?.handleDelete; - const deletingItems = (hookData as any)?.deletingItems || new Set(); - const startNewChat = (hookData as any)?.startNewChat; - - // Get thread preview text for display - const getThreadPreview = (thread: any): string => { - if (thread.name) return thread.name; - if (thread.firstMessage) return thread.firstMessage.substring(0, 50) + (thread.firstMessage.length > 50 ? '...' : ''); - return t('chat_history.no_message_content', 'No message content available'); - }; - - // Prepare data for FormGeneratorList - add a display name field - const threadsWithDisplayName = threads.map((thread: any) => ({ - ...thread, - displayName: getThreadPreview(thread) - })); - - // Define fields for FormGeneratorList - only show creation time - const fields = [ - { - key: 'displayName', - label: '', // No label needed, will be shown as first field - type: 'string' as const - }, - { - key: 'startedAt', - label: '', // No label needed, will be shown as metadata - type: 'date' as const - } - ]; - - // Ensure hookData has required properties for DeleteActionButton - const enhancedHookData = { - ...hookData, - refetch: (hookData as any)?.refetch || (hookData as any)?.loadThreads || (() => {}), - handleDelete: handleDelete || (() => Promise.resolve(false)), - removeOptimistically: (hookData as any)?.removeOptimistically || (hookData as any)?.removeThreadOptimistically, - deletingItems: deletingItems - }; - - // Configure action buttons - delete button (always show) - const actionButtons = [ - { - type: 'delete' as const, - disabled: (row: any) => !handleDelete || deletingItems.has(row.id), - loading: (row: any) => deletingItems.has(row.id), - title: t('chat_history.delete_tooltip', 'Delete workflow'), - operationName: 'handleDelete', - loadingStateName: 'deletingItems' - } - ]; - - // Handle item click to select thread (checkbox and delete button stop propagation) - const handleItemClick = (row: any) => { - if (!deletingItems.has(row.id) && selectThread) { - selectThread(row.id); - } - }; - - // Get data attributes for styling selected items - const getItemDataAttributes = (row: any) => { - return { - 'selected-thread-id': row.id === selectedThreadId ? 'true' : 'false' - }; - }; - - // Show error state if there's an error - if (threadsError) { - return ( -
    -
    -

    {t('chat_history.error_loading', 'Error loading workflows:')} {threadsError}

    - {(hookData as any)?.loadThreads && ( - - )} -
    -
    - ); - } - - return ( -
    - { - // Handle multiple delete - for (const rowToDelete of rowsToDelete) { - await handleDelete(rowToDelete.id); - } - } : undefined} - hookData={enhancedHookData} - getItemDataAttributes={getItemDataAttributes} - searchable={false} - filterable={false} - sortable={false} - pagination={false} - selectable={true} - className={styles.chatHistoryList} - headerButton={startNewChat ? ( - - ) : undefined} - /> -
    - ); - } - - default: - return null; - } - }; - - // Create enhanced drag drop config with hook data integration - const getDragDropConfig = () => { - if (!pageData.dragDropConfig) { - return { enabled: false, onDrop: () => {} }; - } - - - // If the page has drag drop config and hook data with handleUpload or handleFileUpload, integrate them - const uploadHandler = hookData?.handleUpload || hookData?.handleFileUpload; - if (uploadHandler) { - return { - ...pageData.dragDropConfig, - onDrop: async (files: File[]) => { - try { - // Process each file through the hook's upload function - for (const file of files) { - if (uploadHandler) { - await uploadHandler(file); - } - } - } catch (error) { - console.error('Error uploading dropped files:', error); - } - } - }; - } - - - // Fallback to the original config - return pageData.dragDropConfig; - }; - - return ( - -
    -
    - {/* Page Header */} -
    -
    -

    {resolveLanguageText(pageData.title, t)}

    - {pageData.subtitle && ( -

    {resolveLanguageText(pageData.subtitle, t)}

    - )} -
    - - {/* Header Buttons */} - {pageData.headerButtons && pageData.headerButtons.length > 0 && ( -
    - {/* Workflow Status - Left of workflow selector */} - {hookData && pageData.headerButtons.some(btn => btn.id === 'workflow-selector') && ( -
    - -
    - )} - {pageData.headerButtons.filter((button) => { - // Filter header buttons based on RBAC permissions - // Check DATA permissions for create buttons - if (button.id.includes('create') || button.id.includes('add') || button.id.includes('new')) { - const permissions = (hookData as any)?.permissions; - if (!permissions) { - // If permissions not loaded yet, show button (will be filtered when permissions load) - return true; - } - - const hasCreate = permissions.create !== 'n' && permissions.view; - - // Log permission check for debugging - if (import.meta.env.DEV) { - console.log(`🔐 Header button permission check (${button.id}):`, { - operation: 'create', - hasPermission: hasCreate, - permissionValue: permissions.create, - view: permissions.view, - fullPermissions: permissions - }); - } - - return hasCreate; - } - - // For other buttons, show them (they may have their own permission checks) - return true; - }).map((button) => { - // Check if this is a dropdown button - if (button.dropdownConfig) { - const dropdownConfig = button.dropdownConfig; - - // Get dropdown data from hookData if dataSource is specified - let items: DropdownSelectItem[] = []; - let selectedItemId: string | number | null = null; - let onSelectHandler: (item: DropdownSelectItem | null) => void | Promise = () => {}; - - if (dropdownConfig.dataSource && hookData) { - // Get items from hookData - if (dropdownConfig.dataSource.itemsProperty) { - const hookItems = (hookData as any)[dropdownConfig.dataSource.itemsProperty]; - if (Array.isArray(hookItems)) { - items = hookItems.map((item: any) => ({ - id: item.id, - label: typeof item.label === 'string' ? item.label : resolveLanguageText(item.label, t), - value: item.value || item, - metadata: item.metadata - })); - } - } - - // Get selectedItemId from hookData - if (dropdownConfig.dataSource.selectedIdProperty) { - selectedItemId = (hookData as any)[dropdownConfig.dataSource.selectedIdProperty] || null; - } - - // Get onSelect handler from hookData - if (dropdownConfig.dataSource.onSelectMethod) { - const hookOnSelect = (hookData as any)[dropdownConfig.dataSource.onSelectMethod!]; - if (typeof hookOnSelect === 'function') { - onSelectHandler = hookOnSelect; - } - } - } else { - // Use dropdownConfig directly - items = dropdownConfig.items.map(item => ({ - id: item.id, - label: typeof item.label === 'string' ? item.label : resolveLanguageText(item.label, t), - value: item.value, - metadata: item.metadata - })); - selectedItemId = dropdownConfig.selectedItemId ?? null; - onSelectHandler = (item: DropdownSelectItem | null) => { - dropdownConfig.onSelect(item, hookData); - }; - } - - // Check if loading state is available from hookData - // Use generic loading property or check for specific loading property from dropdownConfig - const isLoading = dropdownConfig.dataSource?.loadingProperty - ? (hookData as any)?.[dropdownConfig.dataSource.loadingProperty] || false - : hookData?.loading || false; - - return ( - - ); - } - - // Check if this is an upload button (has handleUpload in hookData) - const handleUpload = (hookData as any)?.handleUpload; - if (import.meta.env.DEV && button.id === 'upload-file') { - console.log('🔍 Upload button check:', { - buttonId: button.id, - hasHandleUpload: !!handleUpload, - hookDataKeys: hookData ? Object.keys(hookData) : 'no hookData', - handleUploadType: typeof handleUpload - }); - } - if (handleUpload && button.id === 'upload-file') { - // Evaluate disabled function if it's a function - let isDisabled = false; - if (button.disabled !== undefined) { - if (typeof button.disabled === 'function') { - try { - const disabledResult = (button.disabled as any)(hookData); - if (typeof disabledResult === 'object' && disabledResult !== null && 'disabled' in disabledResult) { - isDisabled = disabledResult.disabled; - } else if (typeof disabledResult === 'boolean') { - isDisabled = disabledResult; - } - } catch (error) { - console.error(`Error evaluating disabled function for button ${button.id}:`, error); - isDisabled = false; - } - } else if (typeof button.disabled === 'boolean') { - isDisabled = button.disabled; - } - } - - return ( - - {resolveLanguageText(button.label, t)} - - ); - } - - // Check if this button has a formConfig (create button) - if (button.formConfig && hookData) { - const createOperation = button.formConfig.createOperationName - ? (hookData as any)[button.formConfig.createOperationName] - : null; - - if (createOperation) { - // Use generateCreateFieldsFromAttributes from backend if available, otherwise fall back to generateEditFieldsFromAttributes - const hookDataAny = hookData as any; - - // Prefer generateCreateFieldsFromAttributes for create forms - const generateFieldsFunction = hookDataAny.generateCreateFieldsFromAttributes || hookDataAny.generateEditFieldsFromAttributes; - - if (!generateFieldsFunction || typeof generateFieldsFunction !== 'function') { - console.error('Create button requires generateCreateFieldsFromAttributes or generateEditFieldsFromAttributes function in hookData'); - return null; - } - - // Create a wrapper for onCreate that ensures attributes are loaded (define before use) - const wrappedCreateOperation = async (formData: any) => { - // Debug: Log form data being submitted - console.warn('🔧 wrappedCreateOperation - formData:', formData); - - // Ensure attributes are loaded before creating (if function exists) - if (hookDataAny.ensureAttributesLoaded && typeof hookDataAny.ensureAttributesLoaded === 'function') { - await hookDataAny.ensureAttributesLoaded(); - } - return await createOperation(formData); - }; - - // Prefer custom formConfig.fields if defined, otherwise use dynamic fields from backend attributes - const customFields = button.formConfig.fields; - const generatedFields = customFields && customFields.length > 0 - ? customFields - : generateFieldsFunction(); - - // Debug: Log which function is used and what fields are generated - console.log('🔧 CreateButton fields generation:', { - hasCustomFields: !!customFields && customFields.length > 0, - hasCreateFields: !!hookDataAny.generateCreateFieldsFromAttributes, - hasEditFields: !!hookDataAny.generateEditFieldsFromAttributes, - generatedFieldsCount: generatedFields?.length || 0, - generatedFieldKeys: generatedFields?.map((f: any) => f.key) || [] - }); - - // Check if attributes are still loading - const attributes = hookDataAny.attributes; - const isLoadingAttributes = hookDataAny.loading || (attributes === undefined); - - // If attributes are loading, show button but disable it - // If attributes loaded but empty, still show button (might be a backend issue) - // Only hide if we're sure attributes won't load (attributes is null/empty and not loading) - if (!generatedFields || generatedFields.length === 0) { - // If attributes are still loading, show button disabled - if (isLoadingAttributes) { - return ( - { - if (hookData.refetch) { - hookData.refetch(); - } - }} - > - {resolveLanguageText(button.label, t)} - - ); - } - - // Attributes loaded but no fields - log warning but still show button disabled - console.warn('No fields generated from backend attributes. Button will be disabled.'); - return ( - { - if (hookData.refetch) { - hookData.refetch(); - } - }} - > - {resolveLanguageText(button.label, t)} - - ); - } - - // Resolve language text for generated fields - const fieldsToUse = generatedFields.map((field: any) => ({ - ...field, - label: resolveLanguageText(field.label, t), - placeholder: field.placeholder ? resolveLanguageText(field.placeholder, t) : undefined - })); - - // Evaluate disabled property if it's a function - const isDisabled = typeof button.disabled === 'function' - ? button.disabled(hookData) - : button.disabled ?? false; - const disabledValue = typeof isDisabled === 'object' && isDisabled !== null && 'disabled' in isDisabled - ? isDisabled.disabled - : Boolean(isDisabled); - - return ( - { - // Refetch data after successful creation - if (hookData.refetch) { - hookData.refetch(); - } - }} - > - {resolveLanguageText(button.label, t)} - - ); - } - } - - // Regular button - // Evaluate disabled function if it's a function - let isDisabled = false; - if (button.disabled !== undefined) { - if (typeof button.disabled === 'function') { - try { - const disabledResult = (button.disabled as any)(hookData); - if (typeof disabledResult === 'object' && disabledResult !== null && 'disabled' in disabledResult) { - isDisabled = disabledResult.disabled; - } else if (typeof disabledResult === 'boolean') { - isDisabled = disabledResult; - } - } catch (error) { - console.error(`Error evaluating disabled function for button ${button.id}:`, error); - isDisabled = false; - } - } else if (typeof button.disabled === 'boolean') { - isDisabled = button.disabled; - } - } - - return ( - - ); - })} -
    - )} -
    - -
    - - {/* Page Content */} -
    -
    - -
    -
    -
    - -
    -
    - ); -}; - -export default PageRenderer; - diff --git a/src/core/PageManager/SidebarProvider.tsx b/src/core/PageManager/SidebarProvider.tsx deleted file mode 100644 index 522c020..0000000 --- a/src/core/PageManager/SidebarProvider.tsx +++ /dev/null @@ -1,483 +0,0 @@ -import React, { createContext, useContext, useState, useEffect } from 'react'; -import { allPageData, SidebarItem } from './data'; -import { useLanguage } from '../../providers/language/LanguageContext'; -import { resolveLanguageText, GenericPageData } from './pageInterface'; -import { usePermissions } from '../../hooks/usePermissions'; -import { FaHome, FaHatWizard, FaBriefcase, FaProjectDiagram } from 'react-icons/fa'; -import { RiFolderSettingsFill } from 'react-icons/ri'; -import { SidebarSubmenuItemData } from '../../components/Sidebar/sidebarTypes'; - -// Configuration for parent groups that don't have a page definition -// Maps parentPath (can be nested like "start.real-estate") to icon and default order -const parentGroupConfig: Record>; - defaultOrder?: number; -}> = { - 'start': { - icon: FaHome, - defaultOrder: 1 - }, - 'workflows': { - icon: FaProjectDiagram, - defaultOrder: 2 - }, - 'trustee': { - icon: FaBriefcase, - defaultOrder: 3 - }, - 'basedata': { - icon: RiFolderSettingsFill, - defaultOrder: 4 - }, - 'admin': { - icon: FaHatWizard, - defaultOrder: 5 - } -}; - -interface SidebarContextType { - sidebarItems: SidebarItem[]; - loading: boolean; - error: string | null; - refreshSidebar: () => Promise; -} - -const SidebarContext = createContext(undefined); - -export const useSidebar = () => { - const context = useContext(SidebarContext); - if (!context) { - throw new Error('useSidebar must be used within a SidebarProvider'); - } - return context; -}; - -interface SidebarProviderProps { - children: React.ReactNode; -} - -export const SidebarProvider: React.FC = ({ children }) => { - const [sidebarItems, setSidebarItems] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - // Get translation function from language context - const { t } = useLanguage(); - const { canView, preloadUiPermissions } = usePermissions(); - - // Helper type for navigation tree nodes - interface NavigationNode { - id: string; - pathSegment: string; - fullPath: string; // Full dot-notation path (e.g., "start.real-estate") - name: string; - icon?: React.ComponentType>; - order: number; - page?: GenericPageData; // If this node represents an actual page - children: Map; // Keyed by path segment - pages: GenericPageData[]; // Direct child pages - } - - // Helper function to resolve node name - const resolveNodeName = (pathSegment: string, fullPath: string, page?: GenericPageData): string => { - if (page) { - return resolveLanguageText(page.name, t); - } - // Try translation key (e.g., "start.real-estate.title") - const translationKey = `${fullPath}.title`; - const translated = t(translationKey); - if (translated !== translationKey) { - return translated; - } - // Try just the segment (e.g., "real-estate.title") - const segmentKey = `${pathSegment}.title`; - const segmentTranslated = t(segmentKey); - if (segmentTranslated !== segmentKey) { - return segmentTranslated; - } - // Fallback to capitalized segment - return pathSegment.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' '); - }; - - // Helper function to resolve node icon - const resolveNodeIcon = (pathSegment: string, fullPath: string, page?: GenericPageData): React.ComponentType> | undefined => { - if (page?.icon) { - return page.icon; - } - // Check parentGroupConfig for nested paths first (e.g., "start.real-estate") - if (parentGroupConfig[fullPath]?.icon) { - return parentGroupConfig[fullPath].icon; - } - // Check parentGroupConfig for top-level segments (e.g., "start") - if (fullPath === pathSegment && parentGroupConfig[pathSegment]?.icon) { - return parentGroupConfig[pathSegment].icon; - } - return undefined; - }; - - // Helper function to resolve node order - const resolveNodeOrder = (pathSegment: string, fullPath: string, page?: GenericPageData, childPages: GenericPageData[] = []): number => { - if (page?.order !== undefined) { - return page.order; - } - // Check parentGroupConfig for top-level segments - if (fullPath === pathSegment && parentGroupConfig[pathSegment]?.defaultOrder !== undefined) { - return parentGroupConfig[pathSegment].defaultOrder!; - } - // Use minimum order of child pages - if (childPages.length > 0) { - const childOrders = childPages.map(p => p.order ?? 0); - return Math.min(...childOrders); - } - return 0; - }; - - // Build navigation tree from page data - const buildNavigationTree = (): Map => { - const rootNodes = new Map(); - - // Process all pages with parent paths - const pagesWithParents = allPageData.filter( - page => page.parentPath && !page.hide && page.showInSidebar !== false - ); - - for (const page of pagesWithParents) { - if (!page.parentPath) continue; - - // Parse parent path segments (e.g., "start.real-estate" -> ["start", "real-estate"]) - const pathSegments = page.parentPath.split('.'); - - // Build path to root, creating nodes as needed - let currentMap = rootNodes; - let currentFullPath = ''; - - for (let i = 0; i < pathSegments.length; i++) { - const segment = pathSegments[i]; - currentFullPath = currentFullPath ? `${currentFullPath}.${segment}` : segment; - - // Get or create node for this segment - if (!currentMap.has(segment)) { - // Check if there's a page for this path segment - const segmentPage = allPageData.find( - p => p.path === currentFullPath && !p.hide - ); - - const node: NavigationNode = { - id: segmentPage?.id || currentFullPath, - pathSegment: segment, - fullPath: currentFullPath, - name: '', // Will be resolved later - icon: undefined, // Will be resolved later - order: 0, // Will be resolved later - page: segmentPage, - children: new Map(), - pages: [] - }; - - currentMap.set(segment, node); - } - - const node = currentMap.get(segment)!; - - // If this is the last segment, add the page as a child page - if (i === pathSegments.length - 1) { - node.pages.push(page); - } - - // Move to next level - currentMap = node.children; - } - } - - // Resolve names, icons, and orders for all nodes - const resolveNode = (node: NavigationNode): void => { - // Resolve children first (bottom-up) - for (const childNode of node.children.values()) { - resolveNode(childNode); - } - - // Resolve this node - node.name = resolveNodeName(node.pathSegment, node.fullPath, node.page); - node.icon = resolveNodeIcon(node.pathSegment, node.fullPath, node.page); - - // Collect all child pages (from direct pages and nested children) - const allChildPages = [...node.pages]; - for (const childNode of node.children.values()) { - if (childNode.page) { - allChildPages.push(childNode.page); - } - allChildPages.push(...childNode.pages); - } - - node.order = resolveNodeOrder(node.pathSegment, node.fullPath, node.page, allChildPages); - }; - - // Resolve all root nodes - for (const node of rootNodes.values()) { - resolveNode(node); - } - - return rootNodes; - }; - - // Convert navigation tree node to sidebar submenu item (recursive) - const nodeToSubmenuItem = async (node: NavigationNode, depth: number = 0): Promise => { - // Filter child pages by RBAC and privilegeChecker - const accessiblePages: GenericPageData[] = []; - for (const page of node.pages) { - try { - const hasRBACAccess = await canView('UI', page.path); - if (!hasRBACAccess) continue; - - if (page.privilegeChecker) { - try { - const hasPrivilege = await page.privilegeChecker(); - if (!hasPrivilege) continue; - } catch (error) { - console.error(`Error checking privilegeChecker for page ${page.path}:`, error); - continue; - } - } - - accessiblePages.push(page); - } catch (error) { - console.error(`Error checking RBAC access for page ${page.path}:`, error); - } - } - - // Process child nodes recursively (increment depth) - const accessibleChildren: SidebarSubmenuItemData[] = []; - for (const childNode of node.children.values()) { - const childItem = await nodeToSubmenuItem(childNode, depth + 1); - if (childItem) { - accessibleChildren.push(childItem); - } - } - - // Combine pages and child nodes, assigning depth - const allChildren: SidebarSubmenuItemData[] = [ - ...accessiblePages.map(page => ({ - id: page.id, - name: resolveLanguageText(page.name, t), - link: `/${page.path}`, - icon: page.icon, - depth: depth + 1 // Child pages are one level deeper - })), - ...accessibleChildren - ]; - - // If no accessible children, don't create this node - if (allChildren.length === 0) { - return null; - } - - // If this node has a page itself, it shouldn't be a navigation node - // But according to requirements: if it has subpages, it is NOT a page itself - // So we create a navigation node without a link - return { - id: node.id, - name: node.name, - link: undefined, // Navigation node - not a clickable page - icon: node.icon, - submenu: allChildren.length > 0 ? allChildren : undefined, - depth: depth // Current depth level - }; - }; - - // Convert navigation tree to sidebar items - const treeToSidebarItems = async (tree: Map): Promise => { - const items: SidebarItem[] = []; - - // Process each root node (depth 0 for top-level items) - for (const node of tree.values()) { - const submenuItem = await nodeToSubmenuItem(node, 0); - if (submenuItem && submenuItem.submenu && submenuItem.submenu.length > 0) { - items.push({ - id: node.id, - name: node.name, - link: undefined, // Navigation node - not a clickable page - icon: node.icon, - moduleEnabled: true, - order: node.order, - submenu: submenuItem.submenu, - depth: 0 // Top-level items have depth 0 - }); - } - } - - return items; - }; - - // Get sidebar items from page data - const getSidebarItems = async (): Promise => { - const items: SidebarItem[] = []; - - // Build navigation tree - const navigationTree = buildNavigationTree(); - - // Convert tree to sidebar items - const treeItems = await treeToSidebarItems(navigationTree); - items.push(...treeItems); - - // Get main pages (no parent path) - const mainPages = allPageData - .filter(page => !page.parentPath && !page.hide && page.showInSidebar !== false) - .sort((a, b) => (a.order || 0) - (b.order || 0)); - - // Process each main page - for (const pageData of mainPages) { - // Check RBAC permissions - try { - const hasRBACAccess = await canView('UI', pageData.path); - if (!hasRBACAccess) { - continue; - } - - // Check client-side privilegeChecker if provided - if (pageData.privilegeChecker) { - try { - const hasPrivilege = await pageData.privilegeChecker(); - if (!hasPrivilege) { - continue; - } - } catch (error) { - console.error(`Error checking privilegeChecker for ${pageData.path}:`, error); - continue; - } - } - } catch (error) { - console.error(`Error checking RBAC access for ${pageData.path}:`, error); - continue; - } - - // Check if this page has subpages (legacy support) - if (pageData.hasSubpages) { - // Find all subpages for this parent - const allSubpages = allPageData.filter(p => - p.parentPath === pageData.path && - !p.hide && - p.showInSidebar !== false - ); - - // Filter subpages by RBAC access - const accessibleSubpages: GenericPageData[] = []; - for (const subpage of allSubpages) { - try { - const hasSubpageRBACAccess = await canView('UI', subpage.path); - if (!hasSubpageRBACAccess) { - continue; - } - - if (subpage.privilegeChecker) { - try { - const hasPrivilege = await subpage.privilegeChecker(); - if (!hasPrivilege) { - continue; - } - } catch (error) { - console.error(`Error checking privilegeChecker for subpage ${subpage.path}:`, error); - continue; - } - } - - accessibleSubpages.push(subpage); - } catch (error) { - console.error(`Error checking RBAC access for subpage ${subpage.path}:`, error); - } - } - - if (accessibleSubpages.length > 0) { - // Create item with submenu (no link since it has subpages) - items.push({ - id: pageData.id, - name: resolveLanguageText(pageData.name, t), - link: undefined, // No link - has subpages, so it's a navigation node - icon: pageData.icon, - moduleEnabled: pageData.moduleEnabled ?? true, - order: pageData.order || 0, - depth: 0, // Top-level items have depth 0 - submenu: accessibleSubpages.map(subpage => ({ - id: subpage.id, - name: resolveLanguageText(subpage.name, t), - link: `/${subpage.path}`, - icon: subpage.icon, - depth: 1 // First level of submenu - })) - }); - } else { - // No accessible subpages, show as regular item - items.push({ - id: pageData.id, - name: resolveLanguageText(pageData.name, t), - link: `/${pageData.path}`, - icon: pageData.icon, - moduleEnabled: pageData.moduleEnabled ?? true, - order: pageData.order || 0, - depth: 0 // Top-level items have depth 0 - }); - } - } else { - // Regular items without subpages - items.push({ - id: pageData.id, - name: resolveLanguageText(pageData.name, t), - link: `/${pageData.path}`, - icon: pageData.icon, - moduleEnabled: pageData.moduleEnabled ?? true, - order: pageData.order || 0, - depth: 0 // Top-level items have depth 0 - }); - } - } - - // Sort all items by order - const sortedItems = items.sort((a, b) => (a.order || 0) - (b.order || 0)); - - return sortedItems; - }; - - // Refresh sidebar items - const refreshSidebar = async () => { - console.log('🔄 SidebarProvider: Refreshing sidebar items...'); - setLoading(true); - setError(null); - - try { - // Preload all UI permissions in a single API call - // This caches all permissions before iterating through pages - await preloadUiPermissions(); - - const items = await getSidebarItems(); - console.log('✅ SidebarProvider: Setting sidebar items:', { - count: items.length, - items: items.map(item => ({ id: item.id, link: item.link, name: item.name })) - }); - setSidebarItems(items); - } catch (err) { - console.error('❌ SidebarProvider: Error refreshing sidebar:', err); - setError(err instanceof Error ? err.message : 'Failed to load sidebar items'); - } finally { - setLoading(false); - } - }; - - // Load sidebar items on mount and when language changes - useEffect(() => { - refreshSidebar(); - }, [t]); - - const contextValue: SidebarContextType = { - sidebarItems, - loading, - error, - refreshSidebar - }; - - return ( - - {children} - - ); -}; - -export default SidebarProvider; diff --git a/src/core/PageManager/data/index.ts b/src/core/PageManager/data/index.ts deleted file mode 100644 index 7b93ccf..0000000 --- a/src/core/PageManager/data/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Export page data and utilities -export * from './pages'; - -// Re-export the page interface -export * from '../pageInterface'; diff --git a/src/core/PageManager/data/pages/admin/mandates.ts b/src/core/PageManager/data/pages/admin/mandates.ts deleted file mode 100644 index 0391957..0000000 --- a/src/core/PageManager/data/pages/admin/mandates.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { useCallback } from 'react'; -import { GenericPageData } from '../../../pageInterface'; -import { FaFileContract, FaPlus } from 'react-icons/fa'; -import { getUserDataCache } from '../../../../../utils/userCache'; -import { useMandates, useMandateOperations } from '../../../../../hooks/useAdminMandates'; -import { isDateTimeType, type AttributeType } from '../../../../../utils/attributeTypeMapper'; -import type { AttributeDefinition } from '../../../../../api/attributesApi'; - -// Helper function to convert attribute definitions to column config -const attributesToColumns = (attributes: AttributeDefinition[]) => { - return attributes.map(attr => { - // Use attributeTypeMapper to check if this is a date/timestamp field - disable filtering for these - const isDateField = isDateTimeType(attr.type as AttributeType); - - return { - key: attr.name, - label: attr.label || attr.name, - type: attr.type || 'string', - width: attr.width || 200, - minWidth: attr.minWidth || 100, - maxWidth: attr.maxWidth || 400, - sortable: attr.sortable !== false, - // Disable filtering for date/timestamp fields - filterable: isDateField ? false : (attr.filterable !== false), - searchable: attr.searchable !== false, - filterOptions: attr.filterOptions - }; - }); -}; - -// Hook factory function for mandates data -const createMandatesHook = () => { - return () => { - const { - data: mandates, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - attributes, - permissions, - pagination, - fetchMandateById, - generateEditFieldsFromAttributes, - generateCreateFieldsFromAttributes, - ensureAttributesLoaded - } = useMandates(); - const { - handleMandateDelete, - handleMandateCreate, - handleMandateUpdate, - handleInlineUpdate, - deletingMandates, - editingMandates, - deleteError, - updateError - } = useMandateOperations(); - - const generatedColumns = attributes && attributes.length > 0 - ? attributesToColumns(attributes) - : undefined; - - // Handle single mandate deletion for FormGenerator - const handleDeleteSingle = useCallback(async (mandate: any) => { - const success = await handleMandateDelete(mandate.id); - - if (success) { - refetch(); - } - }, [handleMandateDelete, refetch]); - - // Handle multiple mandate deletion for FormGenerator - const handleDeleteMultiple = useCallback(async (selectedMandates: any[]) => { - const mandateIds = selectedMandates.map(mandate => mandate.id); - const results = await Promise.all( - mandateIds.map(id => handleMandateDelete(id)) - ); - - const allSuccessful = results.every((result: boolean) => result); - - if (allSuccessful) { - refetch(); - } - }, [handleMandateDelete, refetch]); - - return { - data: mandates, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - // Operations - handleDelete: handleMandateDelete, - handleDeleteMultiple, - handleMandateUpdate, - handleInlineUpdate, // For inline boolean editing in table - // FormGenerator specific handlers - onDelete: handleDeleteSingle, - onDeleteMultiple: handleDeleteMultiple, - // Loading states - deletingMandates, - editingMandates, - // Error states - deleteError, - updateError, - // Attributes and permissions for dynamic column/button generation - attributes, - permissions, - pagination, // Pagination metadata from backend - columns: generatedColumns, - // Functions for EditActionButton - fetchMandateById, - generateEditFieldsFromAttributes, - generateCreateFieldsFromAttributes, - ensureAttributesLoaded, - // Entity type for EditActionButton - entityType: 'Mandate', - // Create operation for CreateButton - handleMandateCreate - }; - }; -}; - -export const mandatesPageData: GenericPageData = { - id: 'admin-mandates', - path: 'admin/mandates', - name: 'admin.mandates.title', - description: 'admin.mandates.description', - - // Parent page - parentPath: 'admin', - showInSidebar: true, - - // Visual - icon: FaFileContract, - title: 'admin.mandates.title', - subtitle: 'admin.mandates.subtitle', - - // Header buttons - headerButtons: [ - { - id: 'add-mandate', - label: 'admin.mandates.new_button', - variant: 'primary', - size: 'md', - icon: FaPlus, - formConfig: { - // Fields will be generated dynamically from attributes via generateCreateFieldsFromAttributes - // PageRenderer will use generateCreateFieldsFromAttributes if available, otherwise generateEditFieldsFromAttributes - fields: [], // Empty array - fields will be generated dynamically from attributes - popupTitle: 'admin.mandates.modal.create.title', - popupSize: 'medium', - createOperationName: 'handleMandateCreate', - successMessage: 'admin.mandates.create.success', - errorMessage: 'admin.mandates.create.error' - } - } - ], - - // Content sections - using generic table approach - content: [ - { - id: 'mandates-table', - type: 'table', - tableConfig: { - hookFactory: createMandatesHook, - // Columns are generated dynamically from attributes via hookData.columns - actionButtons: [ - { - type: 'edit', - title: 'admin.mandates.action.edit', - idField: 'id', - nameField: 'id', - operationName: 'handleMandateUpdate', - loadingStateName: 'editingMandates', - fetchItemFunctionName: 'fetchMandateById', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view; - return { disabled: !hasUpdate, message: 'No permission to edit mandates' }; - } - }, - { - type: 'delete', - title: 'admin.mandates.action.delete', - idField: 'id', - operationName: 'handleDelete', - loadingStateName: 'deletingMandates', - // Only show if user has delete permission (permissions.delete !== 'n') - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view; - return { disabled: !hasDelete, message: 'No permission to delete mandates' }; - } - } - ], - searchable: true, - filterable: true, - sortable: true, - resizable: true, - pagination: true, - pageSize: 10, - className: 'mandates-table' - } - } - ], - - // Page behavior - persistent: false, - preload: false, - preserveState: true, - moduleEnabled: true, - - // Privilege checker: only allow sysadmin role - privilegeChecker: async () => { - const userData = getUserDataCache(); - const roleLabels = Array.isArray(userData?.roleLabels) ? userData.roleLabels : []; - // Only allow access if user has "sysadmin" role - return roleLabels.includes('sysadmin'); - }, - - // Lifecycle hooks - onActivate: async () => { - if (import.meta.env.DEV) console.log('Mandates page activated'); - }, - onDeactivate: async () => { - if (import.meta.env.DEV) console.log('Mandates page deactivated'); - } -}; diff --git a/src/core/PageManager/data/pages/admin/rbac-role.ts b/src/core/PageManager/data/pages/admin/rbac-role.ts deleted file mode 100644 index 62d5bc4..0000000 --- a/src/core/PageManager/data/pages/admin/rbac-role.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { useCallback } from 'react'; -import { GenericPageData } from '../../../pageInterface'; -import { FaUserShield, FaPlus } from 'react-icons/fa'; -import { getUserDataCache } from '../../../../../utils/userCache'; -import { useRbacRoles, useRbacRoleOperations } from '../../../../../hooks/useAdminRbacRoles'; -import { isDateTimeType, type AttributeType } from '../../../../../utils/attributeTypeMapper'; -import type { AttributeDefinition } from '../../../../../api/attributesApi'; - -// Helper function to convert attribute definitions to column config -const attributesToColumns = (attributes: AttributeDefinition[]) => { - return attributes.map(attr => { - // Use attributeTypeMapper to check if this is a date/timestamp field - disable filtering for these - const isDateField = isDateTimeType(attr.type as AttributeType); - - return { - key: attr.name, - label: attr.label || attr.name, - type: attr.type || 'string', - width: attr.width || 200, - minWidth: attr.minWidth || 100, - maxWidth: attr.maxWidth || 400, - sortable: attr.sortable !== false, - // Disable filtering for date/timestamp fields - filterable: isDateField ? false : (attr.filterable !== false), - searchable: attr.searchable !== false, - filterOptions: attr.filterOptions - }; - }); -}; - -// Hook factory function for rbac roles data -const createRbacRolesHook = () => { - return () => { - const { - data: roles, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - attributes, - permissions, - pagination, - fetchRoleById, - generateEditFieldsFromAttributes, - generateCreateFieldsFromAttributes, - ensureAttributesLoaded - } = useRbacRoles(); - const { - handleRoleDelete, - handleRoleCreate, - handleRoleUpdate, - deletingRoles, - editingRoles, - deleteError, - updateError - } = useRbacRoleOperations(); - - const generatedColumns = attributes && attributes.length > 0 - ? attributesToColumns(attributes) - : undefined; - - // Handle single role deletion for FormGenerator - const handleDeleteSingle = useCallback(async (role: any) => { - const success = await handleRoleDelete(role.id); - - if (success) { - refetch(); - } - }, [handleRoleDelete, refetch]); - - // Handle multiple role deletion for FormGenerator - const handleDeleteMultiple = useCallback(async (selectedRoles: any[]) => { - const roleIds = selectedRoles.map(role => role.id); - const results = await Promise.all( - roleIds.map(id => handleRoleDelete(id)) - ); - - const allSuccessful = results.every((result: boolean) => result); - - if (allSuccessful) { - refetch(); - } - }, [handleRoleDelete, refetch]); - - // Wrapper for create operation that refetches after success - const handleRoleCreateWithRefetch = useCallback(async (roleData: any) => { - const result = await handleRoleCreate(roleData); - if (result?.success) { - await refetch(); - } - return result; - }, [handleRoleCreate, refetch]); - - return { - data: roles, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - // Operations - handleDelete: handleRoleDelete, - handleDeleteMultiple, - handleRoleUpdate, - // FormGenerator specific handlers - onDelete: handleDeleteSingle, - onDeleteMultiple: handleDeleteMultiple, - // Loading states - deletingRoles, - editingRoles, - // Error states - deleteError, - updateError, - // Attributes and permissions for dynamic column/button generation - attributes, - permissions, - pagination, // Pagination metadata from backend - columns: generatedColumns, - // Functions for EditActionButton - fetchRoleById, - generateEditFieldsFromAttributes, - generateCreateFieldsFromAttributes, - ensureAttributesLoaded, - // Entity type for EditActionButton - entityType: 'Role', - // Create operation for CreateButton (with refetch) - handleRoleCreate: handleRoleCreateWithRefetch - }; - }; -}; - -export const rbacRolePageData: GenericPageData = { - id: 'admin-rbac-role', - path: 'admin/rbac-role', - name: 'admin.rbac-role.title', - description: 'admin.rbac-role.description', - - // Parent page - parentPath: 'admin', - showInSidebar: true, - - // Visual - icon: FaUserShield, - title: 'admin.rbac-role.title', - subtitle: 'admin.rbac-role.subtitle', - - // Header buttons - headerButtons: [ - { - id: 'add-role', - label: 'admin.rbac-role.new_button', - variant: 'primary', - size: 'md', - icon: FaPlus, - formConfig: { - // Fields will be generated dynamically from attributes via generateCreateFieldsFromAttributes - // PageRenderer will use generateCreateFieldsFromAttributes if available, otherwise generateEditFieldsFromAttributes - fields: [], // Empty array - fields will be generated dynamically from attributes - popupTitle: 'admin.rbac-role.modal.create.title', - popupSize: 'medium', - createOperationName: 'handleRoleCreate', - successMessage: 'admin.rbac-role.create.success', - errorMessage: 'admin.rbac-role.create.error' - } - } - ], - - // Content sections - using generic table approach - content: [ - { - id: 'rbac-role-description', - type: 'paragraph', - content: 'admin.rbac-role.description_text' - }, - { - id: 'rbac-roles-table', - type: 'table', - tableConfig: { - hookFactory: createRbacRolesHook, - // Columns are generated dynamically from attributes via hookData.columns - actionButtons: [ - { - type: 'edit', - title: 'admin.rbac-role.action.edit', - idField: 'id', - nameField: 'id', - operationName: 'handleRoleUpdate', - loadingStateName: 'editingRoles', - fetchItemFunctionName: 'fetchRoleById', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view; - return { disabled: !hasUpdate, message: 'No permission to edit roles' }; - } - }, - { - type: 'delete', - title: 'admin.rbac-role.action.delete', - idField: 'id', - operationName: 'handleDelete', - loadingStateName: 'deletingRoles', - // Only show if user has delete permission (permissions.delete !== 'n') - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view; - return { disabled: !hasDelete, message: 'No permission to delete roles' }; - } - } - ], - searchable: true, - filterable: true, - sortable: true, - resizable: true, - pagination: true, - pageSize: 10, - className: 'rbac-roles-table' - } - } - ], - - // Page behavior - persistent: false, - preload: false, - preserveState: true, - moduleEnabled: true, - - // Privilege checker: only allow sysadmin role - privilegeChecker: async () => { - const userData = getUserDataCache(); - const roleLabels = Array.isArray(userData?.roleLabels) ? userData.roleLabels : []; - // Only allow access if user has "sysadmin" role - return roleLabels.includes('sysadmin'); - }, - - // Lifecycle hooks - onActivate: async () => { - if (import.meta.env.DEV) console.log('RBAC Role page activated'); - }, - onDeactivate: async () => { - if (import.meta.env.DEV) console.log('RBAC Role page deactivated'); - } -}; diff --git a/src/core/PageManager/data/pages/admin/rbac-rules.ts b/src/core/PageManager/data/pages/admin/rbac-rules.ts deleted file mode 100644 index e8406e0..0000000 --- a/src/core/PageManager/data/pages/admin/rbac-rules.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { useCallback } from 'react'; -import { GenericPageData } from '../../../pageInterface'; -import { FaShieldAlt, FaPlus } from 'react-icons/fa'; -import { getUserDataCache } from '../../../../../utils/userCache'; -import { useRbacRules, useRbacRuleOperations } from '../../../../../hooks/useAdminRbacRules'; -import { isDateTimeType, type AttributeType } from '../../../../../utils/attributeTypeMapper'; -import type { AttributeDefinition } from '../../../../../api/attributesApi'; - -// Helper function to convert attribute definitions to column config -const attributesToColumns = (attributes: AttributeDefinition[]) => { - return attributes.map(attr => { - // Use attributeTypeMapper to check if this is a date/timestamp field - disable filtering for these - const isDateField = isDateTimeType(attr.type as AttributeType); - - return { - key: attr.name, - label: attr.label || attr.name, - type: attr.type || 'string', - width: attr.width || 200, - minWidth: attr.minWidth || 100, - maxWidth: attr.maxWidth || 400, - sortable: attr.sortable !== false, - // Disable filtering for date/timestamp fields - filterable: isDateField ? false : (attr.filterable !== false), - searchable: attr.searchable !== false, - filterOptions: attr.filterOptions - }; - }); -}; - -// Hook factory function for RBAC rules data -const createRbacRulesHook = () => { - return () => { - const { - data: rbacRules, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - attributes, - permissions, - pagination, - fetchRbacRuleById, - generateEditFieldsFromAttributes, - generateCreateFieldsFromAttributes, - ensureAttributesLoaded - } = useRbacRules(); - const { - handleRbacRuleDelete, - handleRbacRuleCreate, - handleRbacRuleUpdate, - handleInlineUpdate, - deletingRbacRules, - editingRbacRules, - deleteError, - updateError - } = useRbacRuleOperations(); - - const generatedColumns = attributes && attributes.length > 0 - ? attributesToColumns(attributes) - : undefined; - - // Handle single RBAC rule deletion for FormGenerator - const handleDeleteSingle = useCallback(async (rule: any) => { - const success = await handleRbacRuleDelete(rule.id); - - if (success) { - refetch(); - } - }, [handleRbacRuleDelete, refetch]); - - // Handle multiple RBAC rule deletion for FormGenerator - const handleDeleteMultiple = useCallback(async (selectedRules: any[]) => { - const ruleIds = selectedRules.map(rule => rule.id); - const results = await Promise.all( - ruleIds.map(id => handleRbacRuleDelete(id)) - ); - - const allSuccessful = results.every((result: boolean) => result); - - if (allSuccessful) { - refetch(); - } - }, [handleRbacRuleDelete, refetch]); - - return { - data: rbacRules, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - // Operations - handleDelete: handleRbacRuleDelete, - handleDeleteMultiple, - handleRbacRuleUpdate, - handleInlineUpdate, // For inline boolean editing in table - // FormGenerator specific handlers - onDelete: handleDeleteSingle, - onDeleteMultiple: handleDeleteMultiple, - // Loading states - deletingRbacRules, - editingRbacRules, - // Error states - deleteError, - updateError, - // Attributes and permissions for dynamic column/button generation - attributes, - permissions, - columns: generatedColumns, - // Functions for EditActionButton - fetchRbacRuleById, - generateEditFieldsFromAttributes, - generateCreateFieldsFromAttributes, - ensureAttributesLoaded, - // Entity type for EditActionButton - entityType: 'AccessRule', - // Create operation for CreateButton - handleRbacRuleCreate, - // Pagination info for FormGeneratorTable - pagination - }; - }; -}; - -export const rbacRulesPageData: GenericPageData = { - id: 'admin-rbac-rules', - path: 'admin/rbac-rules', - name: 'admin.rbac-rules.title', - description: 'admin.rbac-rules.description', - - // Parent page - parentPath: 'admin', - showInSidebar: true, - - // Visual - icon: FaShieldAlt, - title: 'admin.rbac-rules.title', - subtitle: 'admin.rbac-rules.subtitle', - - // Header buttons - headerButtons: [ - { - id: 'add-rbac-rule', - label: 'admin.rbac-rules.new_button', - variant: 'primary', - size: 'md', - icon: FaPlus, - formConfig: { - // Fields will be generated dynamically from attributes via generateCreateFieldsFromAttributes - // PageRenderer will use generateCreateFieldsFromAttributes if available, otherwise generateEditFieldsFromAttributes - fields: [], // Empty array - fields will be generated dynamically from attributes - popupTitle: 'admin.rbac-rules.modal.create.title', - popupSize: 'medium', - createOperationName: 'handleRbacRuleCreate', - successMessage: 'admin.rbac-rules.create.success', - errorMessage: 'admin.rbac-rules.create.error' - } - } - ], - - // Content sections - using generic table approach - content: [ - { - id: 'rbac-rules-table', - type: 'table', - tableConfig: { - hookFactory: createRbacRulesHook, - // Columns are generated dynamically from attributes via hookData.columns - actionButtons: [ - { - type: 'edit', - title: 'admin.rbac-rules.action.edit', - idField: 'id', - nameField: 'id', - operationName: 'handleRbacRuleUpdate', - loadingStateName: 'editingRbacRules', - fetchItemFunctionName: 'fetchRbacRuleById', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view; - return { disabled: !hasUpdate, message: 'No permission to edit RBAC rules' }; - } - }, - { - type: 'delete', - title: 'admin.rbac-rules.action.delete', - idField: 'id', - operationName: 'handleDelete', - loadingStateName: 'deletingRbacRules', - // Only show if user has delete permission (permissions.delete !== 'n') - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view; - return { disabled: !hasDelete, message: 'No permission to delete RBAC rules' }; - } - } - ], - searchable: true, - filterable: true, - sortable: true, - resizable: true, - pagination: true, - pageSize: 10, - className: 'rbac-rules-table' - } - } - ], - - // Page behavior - persistent: false, - preload: false, - preserveState: true, - moduleEnabled: true, - - // Privilege checker: only allow sysadmin role - privilegeChecker: async () => { - const userData = getUserDataCache(); - const roleLabels = Array.isArray(userData?.roleLabels) ? userData.roleLabels : []; - // Only allow access if user has "sysadmin" role - return roleLabels.includes('sysadmin'); - }, - - // Lifecycle hooks - onActivate: async () => { - if (import.meta.env.DEV) console.log('RBAC Rules page activated'); - }, - onDeactivate: async () => { - if (import.meta.env.DEV) console.log('RBAC Rules page deactivated'); - } -}; diff --git a/src/core/PageManager/data/pages/admin/team-members.ts b/src/core/PageManager/data/pages/admin/team-members.ts deleted file mode 100644 index 8a44348..0000000 --- a/src/core/PageManager/data/pages/admin/team-members.ts +++ /dev/null @@ -1,267 +0,0 @@ -import React, { useCallback } from 'react'; -import { GenericPageData } from '../../../pageInterface'; -import { FaUsers, FaPlus } from 'react-icons/fa'; -import { IoMailOutline } from 'react-icons/io5'; -import { useOrgUsers, useUserOperations } from '../../../../../hooks/useUsers'; -import { getUserDataCache } from '../../../../../utils/userCache'; - -// Helper function to convert attribute definitions to column config -const attributesToColumns = (attributes: any[]) => { - return attributes.map(attr => { - // Check if this is a date/timestamp field - disable filtering for these - const isDateField = attr.type === 'date' || - /(date|Date|timestamp|Timestamp|created|Created|updated|Updated|at|At)$/i.test(attr.name); - - return { - key: attr.name, - label: attr.label || attr.name, - type: attr.type || 'string', - width: attr.width || 200, - minWidth: attr.minWidth || 100, - maxWidth: attr.maxWidth || 400, - sortable: attr.sortable !== false, - // Disable filtering for date/timestamp fields - filterable: isDateField ? false : (attr.filterable !== false), - searchable: attr.searchable !== false, - filterOptions: attr.filterOptions - }; - }); -}; - -// Hook factory function for users data -const createUsersHook = () => { - return () => { - const { - data: users, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - attributes, - permissions, - pagination, - fetchUserById, - generateEditFieldsFromAttributes, - generateCreateFieldsFromAttributes, - ensureAttributesLoaded - } = useOrgUsers(); - const { - handleUserDelete, - handleUserCreate, - handleUserUpdate, - handleInlineUpdate, - handleSendPasswordLink, - deletingUsers, - editingUsers, - sendingPasswordLink, - creatingUser, - deleteError, - createError, - updateError, - passwordLinkError - } = useUserOperations(); - - const generatedColumns = attributes && attributes.length > 0 - ? attributesToColumns(attributes) - : undefined; - - // Wrapped create handler that adds mandateId from currentUser cache - const wrappedHandleUserCreate = useCallback(async (formData: any) => { - return await handleUserCreate(formData); - }, [handleUserCreate]); - - // Handle single user deletion for FormGenerator - const handleDeleteSingle = useCallback(async (user: any) => { - const success = await handleUserDelete(user.id); - - if (success) { - refetch(); - } - }, [handleUserDelete, refetch]); - - // Handle multiple user deletion for FormGenerator - const handleDeleteMultiple = useCallback(async (selectedUsers: any[]) => { - const userIds = selectedUsers.map(user => user.id); - const results = await Promise.all( - userIds.map(id => handleUserDelete(id)) - ); - - const allSuccessful = results.every(result => result); - - if (allSuccessful) { - refetch(); - } - }, [handleUserDelete, refetch]); - - return { - data: users, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - // Operations - handleDelete: handleUserDelete, - handleDeleteMultiple, - handleUserCreate: wrappedHandleUserCreate, - handleUserUpdate, - handleInlineUpdate, // For inline boolean editing in table - handleSendPasswordLink, // Send password setup link to user - // FormGenerator specific handlers - onDelete: handleDeleteSingle, - onDeleteMultiple: handleDeleteMultiple, - // Loading states - deletingUsers, - editingUsers, - sendingPasswordLink, - creatingUser, - // Error states - deleteError, - createError, - updateError, - passwordLinkError, - // Attributes and permissions for dynamic column/button generation - attributes, - permissions, - pagination, // Pagination metadata from backend - columns: generatedColumns, - // Functions for EditActionButton - fetchUserById, - generateEditFieldsFromAttributes, - generateCreateFieldsFromAttributes, - ensureAttributesLoaded - }; - }; -}; - -export const teamMembersPageData: GenericPageData = { - id: 'admin-team-members', - path: 'admin/team-members', - name: 'team-members.title', - description: 'team-members.description', - - // Parent page - parentPath: 'admin', - - // Visual - icon: FaUsers, - title: 'team-members.title', - subtitle: 'team-members.subtitle', - - // Header buttons - headerButtons: [ - { - id: 'add-member', - label: 'team-members.new_button', - variant: 'primary', - size: 'md', - icon: FaPlus, - formConfig: { - // Fields will be generated dynamically from attributes via generateCreateFieldsFromAttributes - // PageRenderer will use generateCreateFieldsFromAttributes if available, otherwise generateEditFieldsFromAttributes - fields: [], // Empty array - fields will be generated dynamically from attributes - popupTitle: 'team-members.modal.create.title', - popupSize: 'medium', - createOperationName: 'handleUserCreate', - successMessage: 'team-members.create.success', - errorMessage: 'team-members.create.error' - } - } - ], - - // Content sections - using generic table approach - content: [ - { - id: 'users-table', - type: 'table', - tableConfig: { - hookFactory: createUsersHook, - // Columns are generated dynamically from attributes via hookData.columns - // Standard action buttons (built-in: edit, delete, view, copy) - actionButtons: [ - { - type: 'edit', - title: 'team-members.action.edit', - idField: 'id', - nameField: 'username', - operationName: 'handleUserUpdate', - loadingStateName: 'editingUsers', - fetchItemFunctionName: 'fetchUserById', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view; - return { disabled: !hasUpdate, message: 'No permission to edit users' }; - } - }, - { - type: 'delete', - title: 'team-members.action.delete', - idField: 'id', - operationName: 'handleDelete', - loadingStateName: 'deletingUsers', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view; - return { disabled: !hasDelete, message: 'No permission to delete users' }; - } - } - ], - // Custom action buttons (entity-specific) - customActions: [ - { - id: 'sendPasswordLink', - icon: React.createElement(IoMailOutline), - title: 'team-members.action.sendPasswordLink', - onClick: async (row: any, hookData: any) => { - if (hookData?.handleSendPasswordLink) { - await hookData.handleSendPasswordLink(row.id); - } - }, - // Only show for users with local authentication (not msft/google) - visible: (row: any) => row.authenticationAuthority === 'local', - disabled: (_row: any, hookData: any) => { - if (!hookData?.permissions) return { disabled: false, message: '' }; - const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view; - return { disabled: !hasUpdate, message: 'No permission to send password link' }; - }, - loading: (row: any, hookData: any) => hookData?.sendingPasswordLink?.has(row.id) || false - } - ], - searchable: true, - filterable: true, - sortable: true, - resizable: true, - pagination: true, - pageSize: 10, - className: 'users-table' - } - } - ], - - // Page behavior - persistent: false, - preload: false, - preserveState: true, // Keep page mounted and prevent refetching - moduleEnabled: true, - - // Privilege checker: only allow sysadmin role - privilegeChecker: async () => { - const userData = getUserDataCache(); - const roleLabels = Array.isArray(userData?.roleLabels) ? userData.roleLabels : []; - // Only allow access if user has "sysadmin" role - return roleLabels.includes('sysadmin'); - }, - - // Lifecycle hooks - onActivate: async () => { - if (import.meta.env.DEV) console.log('Team Members activated'); - }, - onLoad: async () => { - if (import.meta.env.DEV) console.log('Team Members loaded - can initialize users list here'); - }, - onUnload: async () => { - if (import.meta.env.DEV) console.log('Team Members unloaded - cleanup users references'); - } -}; - diff --git a/src/core/PageManager/data/pages/automations.ts b/src/core/PageManager/data/pages/automations.ts deleted file mode 100644 index d5326b5..0000000 --- a/src/core/PageManager/data/pages/automations.ts +++ /dev/null @@ -1,282 +0,0 @@ -import { useCallback } from 'react'; -import { GenericPageData } from '../../pageInterface'; -import { FaCog, FaPlus } from 'react-icons/fa'; -import { useAutomations, useAutomationOperations } from '../../../../hooks/useAutomations'; - -// Helper function to convert attribute definitions to column config -const attributesToColumns = (attributes: any[]) => { - return attributes - .filter(attr => { - // Exclude template, placeholders and complex fields from table display - const attrNameLower = attr.name.toLowerCase(); - const excludedColumns = ['template', 'placeholders', 'executionlogs', 'execution_logs']; - return !excludedColumns.includes(attrNameLower); - }) - .map(attr => { - const attrNameLower = attr.name.toLowerCase(); - const isDateField = attr.type === 'date' || - /(date|Date|timestamp|Timestamp|created|Created|updated|Updated|at|At)$/i.test(attr.name); - - const column: any = { - key: attr.name, - label: attr.label || attr.name, - type: attr.type || 'string', - width: attr.width || 200, - minWidth: attr.minWidth || 100, - maxWidth: attr.maxWidth || 400, - sortable: attr.sortable !== false, - filterable: isDateField ? false : (attr.filterable !== false), - searchable: attr.searchable !== false, - filterOptions: attr.filterOptions - }; - - // Format schedule field - if (attrNameLower === 'schedule') { - column.formatter = (value: any) => { - if (!value) return '-'; - const scheduleLabels: Record = { - '0 */4 * * *': 'Every 4 hours', - '0 22 * * *': 'Daily at 22:00', - '0 10 * * 1': 'Weekly Monday 10:00' - }; - return scheduleLabels[value] || value; - }; - } - - // Format active field as badge - if (attrNameLower === 'active') { - column.type = 'boolean'; - } - - // Format placeholders as count - if (attrNameLower === 'placeholders') { - column.formatter = (value: any) => { - if (!value) return '-'; - let obj; - if (typeof value === 'string') { - try { - obj = JSON.parse(value); - } catch { - return '-'; - } - } else if (typeof value === 'object') { - obj = value; - } else { - return '-'; - } - const count = Object.keys(obj).length; - return `${count} placeholder${count !== 1 ? 's' : ''}`; - }; - } - - return column; - }); -}; - -// Hook factory function for automations data -const createAutomationsHook = () => { - return () => { - const { - automations, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - attributes, - permissions, - pagination, - fetchAutomationById, - generateEditFieldsFromAttributes, - generateCreateFieldsFromAttributes, - ensureAttributesLoaded - } = useAutomations(); - const { - handleAutomationDelete, - handleAutomationCreate, - handleAutomationUpdate, - handleAutomationExecute, - handleAutomationToggleActive, - deletingAutomations, - creatingAutomation, - executingAutomations, - deleteError, - createError, - updateError - } = useAutomationOperations(); - - const generatedColumns = attributes && attributes.length > 0 - ? attributesToColumns(attributes) - : undefined; - - // Handle single automation deletion - const handleDeleteSingle = useCallback(async (automation: any) => { - const success = await handleAutomationDelete(automation.id); - if (success) { - refetch(); - } - }, [handleAutomationDelete, refetch]); - - // Handle multiple automation deletion - const handleDeleteMultiple = useCallback(async (selectedAutomations: any[]) => { - const results = await Promise.all( - selectedAutomations.map(a => handleAutomationDelete(a.id)) - ); - const allSuccessful = results.every(result => result); - if (allSuccessful) { - refetch(); - } - }, [handleAutomationDelete, refetch]); - - // Wrapped create handler - const wrappedHandleAutomationCreate = useCallback(async (formData: any) => { - return await handleAutomationCreate(formData); - }, [handleAutomationCreate]); - - return { - data: automations, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - // Operations - handleDelete: handleAutomationDelete, - handleDeleteMultiple, - handleAutomationCreate: wrappedHandleAutomationCreate, - handleAutomationUpdate, - handleAutomationExecute, - handleAutomationToggleActive, - // FormGenerator specific handlers - onDelete: handleDeleteSingle, - onDeleteMultiple: handleDeleteMultiple, - // Loading states - deletingAutomations, - creatingAutomation, - executingAutomations, - // Error states - deleteError, - createError, - updateError, - // Attributes and permissions - attributes, - permissions, - pagination, - columns: generatedColumns, - // Functions for EditActionButton - fetchAutomationById, - generateEditFieldsFromAttributes, - generateCreateFieldsFromAttributes, - ensureAttributesLoaded - }; - }; -}; - -export const automationsPageData: GenericPageData = { - id: 'workflows-automations', - path: 'workflows/automations', - name: 'automations.title', - description: 'automations.description', - - // Parent page - under 'workflows' group - parentPath: 'workflows', - - // Visual - icon: FaCog, - title: 'automations.title', - subtitle: 'automations.subtitle', - - // Header buttons - headerButtons: [ - { - id: 'new-automation', - label: 'automations.new_button', - icon: FaPlus, - variant: 'primary', - formConfig: { - fields: [], // Fields will be generated dynamically from attributes - popupTitle: 'automations.modal.create.title', - popupSize: 'large', - createOperationName: 'handleAutomationCreate', - successMessage: 'automations.create.success', - errorMessage: 'automations.create.error' - } - } - ], - - // Content sections - using generic table approach - content: [ - { - id: 'automations-table', - type: 'table', - tableConfig: { - hookFactory: createAutomationsHook, - // Columns are generated dynamically from attributes via hookData.columns - actionButtons: [ - { - type: 'play', - title: 'automations.action.execute', - idField: 'id', - operationName: 'handleAutomationExecute', - loadingStateName: 'executingAutomations', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasExecute = hookData.permissions.update !== 'n' && hookData.permissions.view; - return { disabled: !hasExecute, message: 'No permission to execute automations' }; - } - }, - { - type: 'edit', - title: 'automations.action.edit', - idField: 'id', - nameField: 'label', - operationName: 'handleAutomationUpdate', - loadingStateName: 'updatingAutomations', - fetchItemFunctionName: 'fetchAutomationById', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view; - return { disabled: !hasUpdate, message: 'No permission to edit automations' }; - } - }, - { - type: 'delete', - title: 'automations.action.delete', - idField: 'id', - operationName: 'handleDelete', - loadingStateName: 'deletingAutomations', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view; - return { disabled: !hasDelete, message: 'No permission to delete automations' }; - } - } - ], - searchable: true, - filterable: true, - sortable: true, - resizable: true, - pagination: true, - pageSize: 10, - className: 'automations-table' - } - } - ], - - // Page behavior - persistent: false, - preload: false, - preserveState: true, - moduleEnabled: true, - - // Lifecycle hooks - onActivate: async () => { - if (import.meta.env.DEV) console.log('Automations activated'); - }, - onLoad: async () => { - if (import.meta.env.DEV) console.log('Automations loaded'); - }, - onUnload: async () => { - if (import.meta.env.DEV) console.log('Automations unloaded'); - } -}; diff --git a/src/core/PageManager/data/pages/chatbot.ts b/src/core/PageManager/data/pages/chatbot.ts deleted file mode 100644 index 68c7f76..0000000 --- a/src/core/PageManager/data/pages/chatbot.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { GenericPageData } from '../../pageInterface'; -import { LuMessageSquare } from 'react-icons/lu'; -import { IoMdSend } from 'react-icons/io'; -import { MdStop } from 'react-icons/md'; -import { createChatbotHook } from '../../../../hooks/useChatbot'; - -export const chatbotPageData: GenericPageData = { - id: 'start-chatbot', - path: 'start/chatbot', - name: 'Chatbot', - description: 'Simple chatbot interface for conversations', - - // Parent page - parentPath: 'start', - - // Visual - icon: LuMessageSquare, - title: 'Chatbot', - subtitle: 'Chat with AI assistant', - - // No header buttons (simpler than dashboard) - headerButtons: [], - - // Content sections - content: [ - { - id: 'chatbot-history', - type: 'chatHistory', - chatHistoryConfig: { - emptyMessage: 'No chat history yet. Start a conversation to see it here.' - } - }, - { - id: 'chatbot-messages', - type: 'messages', - messagesConfig: { - variant: 'chat', - showDocuments: true, - showMetadata: false, - showProgress: false, - emptyMessage: 'No messages yet. Start a conversation to see messages here.' - } - }, - { - id: 'chatbot-input', - type: 'inputForm', - inputFormConfig: { - hookFactory: createChatbotHook, - placeholder: 'Type your message here...', - buttonLabel: 'Send', - stopButtonLabel: 'Stop', - buttonIcon: IoMdSend, - stopButtonIcon: MdStop, - buttonVariant: 'primary', - stopButtonVariant: 'danger', - buttonSize: 'md', - textFieldSize: 'md', - showFileUpload: false - } - } - ], - - // Page behavior - persistent: true, - preserveState: true, - preload: true, - moduleEnabled: true, - - - - // Lifecycle hooks - onActivate: async () => { - if (import.meta.env.DEV) console.log('Chatbot activated - state preserved'); - }, - onDeactivate: async () => { - if (import.meta.env.DEV) console.log('Chatbot deactivated - keeping state'); - } -}; - diff --git a/src/core/PageManager/data/pages/connections.ts b/src/core/PageManager/data/pages/connections.ts deleted file mode 100644 index e335902..0000000 --- a/src/core/PageManager/data/pages/connections.ts +++ /dev/null @@ -1,295 +0,0 @@ -import React, { useCallback } from 'react'; -import { GenericPageData } from '../../pageInterface'; -import { FaGoogle, FaMicrosoft, FaLink, FaSync, FaPlug } from 'react-icons/fa'; -import { useConnections } from '../../../../hooks/useConnections'; - -// Helper function to convert attribute definitions to column config -const attributesToColumns = (attributes: any[]) => { - return attributes.map(attr => ({ - key: attr.name, - label: attr.label || attr.name, - type: attr.type || 'string', - width: attr.width || 200, - minWidth: attr.minWidth || 100, - maxWidth: attr.maxWidth || 400, - sortable: attr.sortable !== false, - filterable: attr.filterable !== false, - searchable: attr.searchable !== false, - filterOptions: attr.filterOptions - })); -}; - -// Hook factory function for connections data -const createConnectionsHook = () => { - return () => { - const { - connections, - fetchConnections, - deleteConnection, - createGoogleConnectionAndAuth, - createMicrosoftConnectionAndAuth, - connectWithPopup, - refreshMicrosoftToken, - refreshGoogleToken, - isConnecting, - isLoading, - error, - attributes, - permissions, - pagination - } = useConnections(); - - const generatedColumns = attributes && attributes.length > 0 - ? attributesToColumns(attributes) - : undefined; - - // Refetch function for pagination-aware refresh - const refetch = useCallback(async (params?: any) => { - await fetchConnections(params); - }, [fetchConnections]); - - // Handle connection deletion - const handleDelete = useCallback(async (connectionId: string) => { - try { - await deleteConnection(connectionId); - // Refresh connections after deletion - FormGenerator will handle pagination - // by calling refetch with current pagination params via its useEffect - return true; - } catch (error) { - console.error('Failed to delete connection:', error); - return false; - } - }, [deleteConnection]); - - // Handle single connection deletion for FormGenerator - const handleDeleteSingle = useCallback(async (connection: any) => { - const success = await handleDelete(connection.id); - if (success) { - refetch(); - } - }, [handleDelete, refetch]); - - // Handle multiple connection deletion for FormGenerator - const handleDeleteMultiple = useCallback(async (selectedConnections: any[]) => { - const connectionIds = selectedConnections.map(conn => conn.id); - const results = await Promise.all( - connectionIds.map(id => handleDelete(id)) - ); - const allSuccessful = results.every(result => result); - if (allSuccessful) { - refetch(); - } - }, [handleDelete, refetch]); - - return { - data: connections, - loading: isLoading, - error: error, - refetch, - // Operations - handleDelete, - // FormGenerator specific handlers - onDelete: handleDeleteSingle, - onDeleteMultiple: handleDeleteMultiple, - connectWithPopup, - createGoogleConnectionAndAuth, - createMicrosoftConnectionAndAuth, - // Token refresh operations - refreshMicrosoftToken, - refreshGoogleToken, - // Loading states - isConnecting, - deletingConnections: new Set(), // Placeholder for consistency with other pages - refreshingConnections: new Set(), // Track which connections are refreshing - // Attributes and permissions for dynamic column/button generation - attributes, - permissions, - columns: generatedColumns, // Return generated columns - pagination - }; - }; -}; - -export const connectionsPageData: GenericPageData = { - id: 'basedata-connections', - path: 'basedata/connections', - name: 'connections.title', - description: 'connections.title', - - // Parent page - parentPath: 'basedata', - - // Visual - icon: FaLink, - title: 'connections.title', - subtitle: 'connections.subtitle', - - // Header buttons - Create Google and Microsoft connections - headerButtons: [ - { - id: 'add-google-connection', - label: 'connections.add_google_button', - icon: FaGoogle, - variant: 'primary', - onClick: async (hookData: any) => { - if (!hookData) { - console.error('No hookData available for Google connection creation'); - return; - } - if (!hookData.createGoogleConnectionAndAuth) { - console.error('createGoogleConnectionAndAuth function not found in hookData', hookData); - return; - } - try { - await hookData.createGoogleConnectionAndAuth(); - // Refresh connections after creation - if (hookData?.refetch) { - await hookData.refetch(); - } - } catch (error) { - console.error('Failed to create Google connection:', error); - } - }, - // Only show if user has create permission - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasCreate = hookData.permissions.create !== 'n' && hookData.permissions.view; - return { disabled: !hasCreate, message: 'No permission to create connections' }; - } - }, - { - id: 'add-microsoft-connection', - label: 'connections.add_microsoft_button', - icon: FaMicrosoft, - variant: 'primary', - onClick: async (hookData: any) => { - if (!hookData) { - console.error('No hookData available for Microsoft connection creation'); - return; - } - if (!hookData.createMicrosoftConnectionAndAuth) { - console.error('createMicrosoftConnectionAndAuth function not found in hookData', hookData); - return; - } - try { - await hookData.createMicrosoftConnectionAndAuth(); - // Refresh connections after creation - if (hookData?.refetch) { - await hookData.refetch(); - } - } catch (error) { - console.error('Failed to create Microsoft connection:', error); - } - }, - // Only show if user has create permission - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasCreate = hookData.permissions.create !== 'n' && hookData.permissions.view; - return { disabled: !hasCreate, message: 'No permission to create connections' }; - } - } - ], - - // Content sections - using generic table approach - content: [ - { - id: 'connections-table', - type: 'table', - tableConfig: { - hookFactory: createConnectionsHook, - // Columns are generated dynamically from attributes via hookData.columns - // Standard action buttons - actionButtons: [ - { - type: 'delete', - title: 'connections.action.delete', - idField: 'id', - operationName: 'handleDelete', - loadingStateName: 'deletingConnections', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view; - return { disabled: !hasDelete, message: 'No permission to delete connections' }; - } - } - ], - // Custom action buttons (entity-specific) - customActions: [ - { - id: 'connect', - icon: React.createElement(FaPlug), - title: 'connections.action.connect', - onClick: async (row: any, hookData: any) => { - if (hookData?.connectWithPopup) { - await hookData.connectWithPopup(row.id); - } - }, - // Only show connect button if status is not 'active' - visible: (row: any) => row.status !== 'active', - disabled: (_row: any, hookData: any) => { - if (!hookData?.permissions) return { disabled: false, message: '' }; - const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view; - return { disabled: !hasUpdate, message: 'No permission to connect' }; - }, - loading: (_row: any, hookData: any) => hookData?.isConnecting || false - }, - { - id: 'refresh', - icon: React.createElement(FaSync), - title: 'connections.action.refresh', - onClick: async (row: any, hookData: any) => { - // Determine which refresh function to use based on authority - if (row.authority === 'msft' && hookData?.refreshMicrosoftToken) { - await hookData.refreshMicrosoftToken(row.id); - if (hookData?.refetch) await hookData.refetch(); - } else if (row.authority === 'google' && hookData?.refreshGoogleToken) { - await hookData.refreshGoogleToken(row.id); - if (hookData?.refetch) await hookData.refetch(); - } - }, - // Only show refresh button if status is 'active' (already connected) - visible: (row: any) => row.status === 'active', - disabled: (_row: any, hookData: any) => { - if (!hookData?.permissions) return { disabled: false, message: '' }; - const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view; - return { disabled: !hasUpdate, message: 'No permission to refresh token' }; - }, - loading: (row: any, hookData: any) => hookData?.refreshingConnections?.has(row.id) || false - } - ], - searchable: true, - filterable: true, - sortable: true, - resizable: true, - pagination: true, - pageSize: 10, - className: 'connections-table' - } - } - ], - - // Page behavior - persistent: false, - preload: false, - preserveState: true, // Keep page mounted and prevent refetching - moduleEnabled: true, - - // Sidebar - will be shown as subpage under Administration - - // No drag and drop for connections - dragDropConfig: { - enabled: false - }, - - // Lifecycle hooks - onActivate: async () => { - if (import.meta.env.DEV) console.log('Connections activated'); - }, - onLoad: async () => { - if (import.meta.env.DEV) console.log('Connections loaded - can initialize connections list here'); - }, - onUnload: async () => { - if (import.meta.env.DEV) console.log('Connections unloaded - cleanup connections references'); - } -}; - diff --git a/src/core/PageManager/data/pages/dashboard.ts b/src/core/PageManager/data/pages/dashboard.ts deleted file mode 100644 index c38e8d7..0000000 --- a/src/core/PageManager/data/pages/dashboard.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { GenericPageData } from '../../pageInterface'; -import { LuTicket } from 'react-icons/lu'; -import { IoMdSend } from 'react-icons/io'; -import { MdStop } from 'react-icons/md'; -import { HiOutlineCollection } from 'react-icons/hi'; -import { createDashboardHook } from '../../../../hooks/usePlayground'; - -export const dashboardPageData: GenericPageData = { - id: 'workflows-playground', - path: 'workflows/playground', - name: 'chatPlayground.title', - description: 'chatPlayground.description', - - // Parent page - now under 'workflows' group - parentPath: 'workflows', - - // Visual - icon: LuTicket, - title: 'chatPlayground.title', - subtitle: 'chatPlayground.subtitle', - - // Header buttons - headerButtons: [ - { - id: 'workflow-selector', - label: 'dashboard.workflow.selector', - variant: 'primary', - size: 'md', - icon: HiOutlineCollection, - dropdownConfig: { - type: 'dropdown', - items: [], // Will be populated from hookData - placeholder: 'dashboard.workflow.select', - emptyMessage: 'dashboard.workflow.empty', - headerText: 'dashboard.workflow.header', - onSelect: () => {}, // Placeholder - actual handler comes from dataSource.onSelectMethod - dataSource: { - itemsProperty: 'workflowItems', - selectedIdProperty: 'selectedWorkflowId', - onSelectMethod: 'onWorkflowSelect' - } - } - } - ], - - // Content sections - content: [ - { - id: 'workflow-messages', - type: 'messages', - messagesConfig: { - showDocuments: true, - showMetadata: false, - showProgress: false, - emptyMessage: 'No messages yet. Start a workflow to see messages here.' - } - }, - { - id: 'workflow-log', - type: 'log', - logConfig: { - emptyMessage: 'No log information available' - } - }, - { - id: 'workflow-input', - type: 'inputForm', - inputFormConfig: { - hookFactory: createDashboardHook, - placeholder: 'dashboard.input.placeholder', - buttonLabel: 'dashboard.button.send', - stopButtonLabel: 'dashboard.button.stop', - buttonIcon: IoMdSend, - stopButtonIcon: MdStop, - buttonVariant: 'primary', - stopButtonVariant: 'danger', - buttonSize: 'md', - textFieldSize: 'md' - } - } - ], - - // Page behavior - persistent: true, - preserveState: true, - preload: true, - moduleEnabled: true, - - - // Lifecycle hooks - onActivate: async () => { - if (import.meta.env.DEV) console.log('Dashboard activated - state preserved'); - }, - onDeactivate: async () => { - if (import.meta.env.DEV) console.log('Dashboard deactivated - keeping state'); - } -}; \ No newline at end of file diff --git a/src/core/PageManager/data/pages/files.ts b/src/core/PageManager/data/pages/files.ts deleted file mode 100644 index d4a5698..0000000 --- a/src/core/PageManager/data/pages/files.ts +++ /dev/null @@ -1,302 +0,0 @@ -import React, { useCallback } from 'react'; -import { GenericPageData } from '../../pageInterface'; -import { FaRegFileAlt, FaUpload, FaDownload } from 'react-icons/fa'; -import { useUserFiles, useFileOperations } from '../../../../hooks/useFiles'; - -// Helper function to convert attribute definitions to column config -const attributesToColumns = (attributes: any[]) => { - return attributes.map(attr => { - // Check if this is a date/timestamp field - disable filtering for these - const isDateField = attr.type === 'date' || - /(date|Date|timestamp|Timestamp|created|Created|updated|Updated|at|At)$/i.test(attr.name); - - const column: any = { - key: attr.name, - label: attr.label || attr.name, - type: attr.type || 'string', - width: attr.width || 200, - minWidth: attr.minWidth || 100, - maxWidth: attr.maxWidth || 400, - sortable: attr.sortable !== false, - // Disable filtering for date/timestamp fields - filterable: isDateField ? false : (attr.filterable !== false), - searchable: attr.searchable !== false, - filterOptions: attr.filterOptions - }; - - // Note: fileHash formatting is now handled automatically by FormGeneratorTable - // with copyable truncated values - no custom formatter needed - - // Add formatter for fileSize to display human-readable sizes - if (attr.name === 'fileSize' || attr.name === 'file_size' || attr.name === 'size') { - column.formatter = (value: any) => { - if (value === null || value === undefined) return '-'; - const bytes = typeof value === 'number' ? value : parseFloat(value); - if (isNaN(bytes) || bytes < 0) return value || '-'; - - // Format bytes to human-readable format - const units = ['B', 'KB', 'MB', 'GB', 'TB']; - let size = bytes; - let unitIndex = 0; - - while (size >= 1024 && unitIndex < units.length - 1) { - size /= 1024; - unitIndex++; - } - - // Round to 2 decimal places, but show integers if possible - const formattedSize = size % 1 === 0 ? size.toString() : size.toFixed(2); - return `${formattedSize} ${units[unitIndex]}`; - }; - } - - return column; - }); -}; - -// Hook factory function for files data -const createFilesHook = () => { - return () => { - const { - data: files, - loading, - error, - refetch, - removeFileOptimistically, - updateFileOptimistically, - attributes, - permissions, - pagination, - fetchFileById, - generateEditFieldsFromAttributes, - ensureAttributesLoaded - } = useUserFiles(); - const { - handleFileDownload, - handleFileDeleteMultiple, - handleFilePreview, - handleFileUpdate, - handleFileDelete, - handleFileUpload, - deletingFiles, - previewingFiles, - editingFiles, - downloadingFiles, - uploadingFile - } = useFileOperations(); - - const generatedColumns = attributes && attributes.length > 0 - ? attributesToColumns(attributes) - : undefined; - - // Handle single file deletion for FormGenerator - const handleDeleteSingle = useCallback(async (file: any) => { - const success = await handleFileDelete(file.id); - - if (success) { - refetch(); - } - }, [handleFileDelete, refetch]); - - // Handle multiple file deletion for FormGenerator - const handleDeleteMultiple = useCallback(async (selectedFiles: any[]) => { - const fileIds = selectedFiles.map(file => file.id); - const success = await handleFileDeleteMultiple(fileIds); - - if (success) { - refetch(); - } - }, [handleFileDeleteMultiple, refetch]); - - // Wrap handleFileUpload for upload button and drag-and-drop - // GenericDataHook expects: (file: File) => Promise<{ success: boolean; data: any }> - const handleUpload = useCallback(async (file: File): Promise<{ success: boolean; data: any }> => { - const result = await handleFileUpload(file); - if (result.success) { - // Refetch files list after successful upload - refetch(); - } - // Map fileData to data to match GenericDataHook interface - return { - success: result.success, - data: 'fileData' in result ? result.fileData : ('data' in result ? result.data : null) - }; - }, [handleFileUpload, refetch]); - - return { - data: files, - loading, - error, - refetch, - removeFileOptimistically, // Expose optimistic removal (generic name for DeleteActionButton) - updateFileOptimistically, // Expose optimistic update for EditActionButton - // Operations - handleDownload: handleFileDownload, - handleDelete: handleFileDelete, - handleDeleteMultiple, - handlePreview: handleFilePreview, - handleFileUpdate, - // FormGenerator specific handlers - onDelete: handleDeleteSingle, - onDeleteMultiple: handleDeleteMultiple, - // Loading states - downloadingFiles, - deletingFiles, - previewingFiles, - editingFiles, - uploadingFile, - // Upload functionality for button and drag-and-drop - handleUpload, - // Attributes and permissions for dynamic column/button generation - attributes, - permissions, - pagination, // Pagination metadata from backend - columns: generatedColumns, // Return generated columns - // Functions for EditActionButton - fetchFileById, // Fetch single file by ID - generateEditFieldsFromAttributes, // Generate edit fields from attributes - ensureAttributesLoaded // Generic function to ensure attributes are loaded - }; - }; -}; - -export const filesPageData: GenericPageData = { - id: 'basedata-files', - path: 'basedata/files', - name: 'files.title', - description: 'files.description', - - // Parent page - now under 'basedata' group (formerly 'administration') - parentPath: 'basedata', - - // Visual - icon: FaRegFileAlt, - title: 'files.title', - subtitle: 'files.title', - - // Header buttons - headerButtons: [ - { - id: 'upload-file', - label: 'files.upload_button', - icon: FaUpload, - variant: 'primary', - // onClick will be handled by PageRenderer to render UploadButton - onClick: () => {}, // Placeholder - PageRenderer will detect this as upload button - // Only show if user has create permission (permissions.create !== 'n') - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasCreate = hookData.permissions.create !== 'n' && hookData.permissions.view; - return { disabled: !hasCreate, message: 'No permission to upload files' }; - } - } - ], - - // Content sections - using generic table approach - content: [ - { - id: 'files-table', - type: 'table', - tableConfig: { - hookFactory: createFilesHook, - // Columns are generated dynamically from attributes via hookData.columns - // Standard action buttons (built-in: edit, delete, view, copy) - actionButtons: [ - { - type: 'view', - title: 'files.action.preview', - idField: 'id', - nameField: 'fileName', - typeField: 'mimeType', - operationName: 'handlePreview', - loadingStateName: 'previewingFiles', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasRead = hookData.permissions.read !== 'n' && hookData.permissions.view; - return { disabled: !hasRead, message: 'No permission to preview files' }; - } - }, - { - type: 'edit', - title: 'files.action.edit', - idField: 'id', - nameField: 'fileName', - operationName: 'handleFileUpdate', - loadingStateName: 'editingFiles', - fetchItemFunctionName: 'fetchFileById', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view; - return { disabled: !hasUpdate, message: 'No permission to edit files' }; - } - }, - { - type: 'delete', - title: 'files.action.delete', - idField: 'id', - operationName: 'handleDelete', - loadingStateName: 'deletingFiles', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view; - return { disabled: !hasDelete, message: 'No permission to delete files' }; - } - } - ], - // Custom action buttons (entity-specific) - customActions: [ - { - id: 'download', - icon: React.createElement(FaDownload), - title: 'files.action.download', - onClick: async (row: any, hookData: any) => { - if (hookData?.handleDownload) { - await hookData.handleDownload(row.id, row.fileName, row.mimeType); - } - }, - disabled: (_row: any, hookData: any) => { - if (!hookData?.permissions) return { disabled: false, message: '' }; - const hasRead = hookData.permissions.read !== 'n' && hookData.permissions.view; - return { disabled: !hasRead, message: 'No permission to download files' }; - }, - loading: (row: any, hookData: any) => hookData?.downloadingFiles?.has(row.id) || false - } - ], - searchable: true, - filterable: true, - sortable: true, - resizable: true, - pagination: true, - pageSize: 10, - className: 'files-table' - } - } - ], - - // Page behavior - persistent: false, - preload: false, - preserveState: true, // Keep page mounted and prevent refetching - moduleEnabled: true, - - // Sidebar - will be shown as subpage under Administration - - // Drag and drop configuration - dragDropConfig: { - enabled: true, - accept: '*/*', // Accept all file types - multiple: true, // Allow multiple files - // overlayText and overlaySubtext will use default translations from DragDropOverlay - }, - - // Lifecycle hooks - onActivate: async () => { - if (import.meta.env.DEV) console.log('Files activated'); - }, - onLoad: async () => { - if (import.meta.env.DEV) console.log('Files loaded - can initialize file lists here'); - }, - onUnload: async () => { - if (import.meta.env.DEV) console.log('Files unloaded - cleanup file references'); - } -}; diff --git a/src/core/PageManager/data/pages/index.ts b/src/core/PageManager/data/pages/index.ts deleted file mode 100644 index 79ddedd..0000000 --- a/src/core/PageManager/data/pages/index.ts +++ /dev/null @@ -1,84 +0,0 @@ -// Export all page data -export { dashboardPageData } from './dashboard'; -export { filesPageData } from './files'; -export { workflowsPageData } from './workflows'; -export { automationsPageData } from './automations'; -export { connectionsPageData } from './connections'; -export { teamMembersPageData } from './admin/team-members'; -export { promptsPageData } from './prompts'; -export { settingsPageData } from './settings'; -export { pekPageData } from './pek'; -export { pekTablesPageData } from './pek-tables'; -export { chatbotPageData } from './chatbot'; -export { mandatesPageData } from './admin/mandates'; -export { rbacRulesPageData } from './admin/rbac-rules'; -export { rbacRolePageData } from './admin/rbac-role'; -// Trustee pages (no container - SidebarProvider creates virtual parent group) -export { - trusteeOrganisationsPageData, - trusteeRolesPageData, - trusteeAccessPageData, - trusteeContractsPageData, - trusteeDocumentsPageData, - trusteePositionsPageData, - trusteePages -} from './trustee'; - -// Import all page data -import { dashboardPageData } from './dashboard'; -import { filesPageData } from './files'; -import { workflowsPageData } from './workflows'; -import { automationsPageData } from './automations'; -import { connectionsPageData } from './connections'; -import { teamMembersPageData } from './admin/team-members'; -import { promptsPageData } from './prompts'; -import { settingsPageData } from './settings'; -import { pekPageData } from './pek'; -import { pekTablesPageData } from './pek-tables'; -import { chatbotPageData } from './chatbot'; -import { mandatesPageData } from './admin/mandates'; -import { rbacRulesPageData } from './admin/rbac-rules'; -import { rbacRolePageData } from './admin/rbac-role'; -import { trusteePages } from './trustee'; - -// Array of all page data -export const allPageData = [ - // Workflows group - dashboardPageData, // Chat Playground - workflowsPageData, // Workflows list - automationsPageData, // Automations - // Basedata group - filesPageData, - promptsPageData, - // Other pages - connectionsPageData, - speechPageData, - settingsPageData, - pekPageData, - pekTablesPageData, - chatbotPageData, - // Trustee pages - ...trusteePages, - // Admin pages - teamMembersPageData, - mandatesPageData, - rbacRulesPageData, - rbacRolePageData, -]; - -// Helper function to get page data by path -export const getPageDataByPath = (path: string) => { - return allPageData.find(page => page.path === path); -}; - -// Helper function to get all pages with subpages organized -export const getPageHierarchy = () => { - const pages = allPageData.filter(page => !page.parentPath); - const subpages = allPageData.filter(page => page.parentPath); - - return { - mainPages: pages, - subpages: subpages, - allPages: allPageData - }; -}; diff --git a/src/core/PageManager/data/pages/pek-tables.ts b/src/core/PageManager/data/pages/pek-tables.ts deleted file mode 100644 index 45e2cfe..0000000 --- a/src/core/PageManager/data/pages/pek-tables.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { GenericPageData } from '../../pageInterface'; -import { FaTable, FaPlus } from 'react-icons/fa'; -import { createProjectsTableHook, createParzellenTableHook } from '../../../../hooks/usePekTables'; -import { getUserDataCache } from '../../../../utils/userCache'; - -export const pekTablesPageData: GenericPageData = { - id: 'pek-tables', - path: 'start/real-estate/pek-tables', - name: 'Projektmanagement', - description: 'Projektmanagement mit Tabellen', - - // Parent page - parentPath: 'start.real-estate', - - // Visual - icon: FaTable, - title: 'Projektmanagement', - subtitle: 'Datenverwaltung', - - // Header buttons - headerButtons: [ - { - id: 'create-project', - label: 'Neues Projekt', - variant: 'primary', - size: 'lg', - icon: FaPlus, - formConfig: { - fields: [], // Will be generated from attributes via generateEditFieldsFromAttributes - popupTitle: 'Neues Projekt erstellen', - popupSize: 'large', - createOperationName: 'handleProjectCreate', - multiStep: true // Enable multi-step form with Step 1 (label) and Step 2 (parcel selection) - }, - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasCreate = hookData.permissions.create !== 'n' && hookData.permissions.view; - return { disabled: !hasCreate, message: 'No permission to create projects' }; - } - } - ], - - // Content sections - content: [ - { - id: 'projektmanagement-layout', - type: 'columns', - columnsConfig: { - columns: [ - { - id: 'main-column', - width: '3fr', - content: [ - { - id: 'tables-tabs', - type: 'tabs', - tabsConfig: { - tabs: [ - { - id: 'projects', - label: 'Projekte', - content: [ - { - id: 'projects-table', - type: 'table', - tableConfig: { - hookFactory: createProjectsTableHook, - searchable: true, - filterable: true, - sortable: true, - resizable: true, - pagination: true, - pageSize: 10, - emptyMessage: 'Noch keine Projekte erstellt, erstelle jetzt dein erstes Projekt!', - actionButtons: [ - { - type: 'edit', - title: 'common.edit', - idField: 'id', - operationName: 'handleProjectUpdate', - loadingStateName: 'editingProjects', - fetchItemFunctionName: 'fetchProjectById', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view; - return { disabled: !hasUpdate, message: 'No permission to edit projects' }; - } - }, - { - type: 'delete', - title: 'common.delete', - idField: 'id', - operationName: 'handleDelete', - loadingStateName: 'deletingProjects', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view; - return { disabled: !hasDelete, message: 'No permission to delete projects' }; - } - } - ] - } - } - ] - }, - { - id: 'parzellen', - label: 'Parzellen', - content: [ - { - id: 'parzellen-table', - type: 'table', - tableConfig: { - hookFactory: createParzellenTableHook, - searchable: true, - filterable: true, - sortable: true, - resizable: true, - pagination: true, - pageSize: 10, - emptyMessage: 'Noch keine Parzellen erstellt, erstelle jetzt dein erstes Projekt und füge eine Parzelle hinzu!', - actionButtons: [ - { - type: 'view', - title: 'common.view', - idField: 'id', - nameField: 'label', - operationName: 'handleParzelleView', - loadingStateName: 'viewingParzellen', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasRead = hookData.permissions.read !== 'n' && hookData.permissions.view; - return { disabled: !hasRead, message: 'No permission to view parzellen' }; - } - }, - { - type: 'edit', - title: 'common.edit', - idField: 'id', - operationName: 'handleParzelleUpdate', - loadingStateName: 'editingParzellen', - fetchItemFunctionName: 'fetchParzelleById', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view; - return { disabled: !hasUpdate, message: 'No permission to edit parzellen' }; - } - }, - { - type: 'delete', - title: 'common.delete', - idField: 'id', - operationName: 'handleDelete', - loadingStateName: 'deletingParzellen', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view; - return { disabled: !hasDelete, message: 'No permission to delete parzellen' }; - } - } - ] - } - } - ] - } - ], - defaultTabId: 'projects' - } - } - ] - }, - { - id: 'sidebar-column', - width: '1fr', - content: [] - } - ], - gap: '1rem' - } - } - ], - - // Page behavior - persistent: false, - preload: false, - preserveState: true, - moduleEnabled: true, - - // Sidebar - order: 11, - - // Privilege checker: deny access for "user" role - privilegeChecker: async () => { - const userData = getUserDataCache(); - const roleLabels = Array.isArray(userData?.roleLabels) ? userData.roleLabels : []; - // Deny access if user has "user" role - return !roleLabels.includes('user'); - }, - - // Lifecycle hooks - onActivate: async () => { - if (import.meta.env.DEV) console.log('PEK Tables page activated'); - }, - onDeactivate: async () => { - if (import.meta.env.DEV) console.log('PEK Tables page deactivated'); - } -}; - diff --git a/src/core/PageManager/data/pages/pek.ts b/src/core/PageManager/data/pages/pek.ts deleted file mode 100644 index b46c6f2..0000000 --- a/src/core/PageManager/data/pages/pek.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { GenericPageData } from '../../pageInterface'; -import { FaBuilding } from 'react-icons/fa'; -import { IoMdSend } from 'react-icons/io'; -import PekLocationInput from './pek/PekLocationInput'; -import PekMapView from './pek/PekMapView'; -import { usePek } from '../../../../hooks/usePek'; -import PekPageWrapper from './pek/PekPageWrapper'; -import { getUserDataCache } from '../../../../utils/userCache'; - -// Hook factory for PEK page -const createPekHook = () => { - return () => { - const pekData = usePek(); - - const handleSubmit = async () => { - await pekData.processCommand(pekData.commandInput); - }; - - return { - // Messages for command results - messages: pekData.commandResults, - // Loading states - loading: pekData.isProcessingCommand || pekData.isSearchingParcel, - error: pekData.commandError || pekData.parcelSearchError || pekData.locationError, - // Empty data array for compatibility - data: [], - // Input form properties (for command input) - inputValue: pekData.commandInput, - onInputChange: pekData.setCommandInput, - handleSubmit, - isSubmitting: pekData.isProcessingCommand - }; - }; -}; - -export const pekPageData: GenericPageData = { - id: 'pek', - path: 'start/real-estate/pek', - name: 'projects.title', - description: 'projects.description', - - // Parent page - parentPath: 'start.real-estate', - - // Visual - icon: FaBuilding, - title: 'projects.title', - subtitle: 'projects.subtitle', - - // Header buttons - headerButtons: [], - - // Content sections - content: [ - { - id: 'pek-description', - type: 'paragraph', - content: 'projects.description_text' - }, - { - id: 'pek-location-input', - type: 'custom', - customComponent: PekLocationInput - }, - { - id: 'pek-map-view', - type: 'custom', - customComponent: PekMapView - }, - { - id: 'pek-command-input', - type: 'inputForm', - inputFormConfig: { - hookFactory: createPekHook, - placeholder: 'projects.command.placeholder', - buttonLabel: 'Senden', - buttonIcon: IoMdSend, - buttonVariant: 'primary', - buttonSize: 'md', - textFieldSize: 'md' - } - }, - { - id: 'pek-command-results', - type: 'messages', - messagesConfig: { - variant: 'chat', - showDocuments: false, - showMetadata: false, - showProgress: false, - emptyMessage: 'projects.command.empty' - } - } - ], - - // Page behavior - persistent: false, - preload: false, - preserveState: true, - moduleEnabled: true, - - // Sidebar - order: 10, - - // Privilege checker: deny access for "user" role - privilegeChecker: async () => { - const userData = getUserDataCache(); - const roleLabels = Array.isArray(userData?.roleLabels) ? userData.roleLabels : []; - // Deny access if user has "user" role - return !roleLabels.includes('user'); - }, - - // Custom component wrapper with PekProvider - customComponent: PekPageWrapper, - - // Lifecycle hooks - onActivate: async () => { - if (import.meta.env.DEV) console.log('PEK page activated'); - }, - onDeactivate: async () => { - if (import.meta.env.DEV) console.log('PEK page deactivated'); - } -}; - diff --git a/src/core/PageManager/data/pages/pek/PekLocationInput.module.css b/src/core/PageManager/data/pages/pek/PekLocationInput.module.css deleted file mode 100644 index 463346b..0000000 --- a/src/core/PageManager/data/pages/pek/PekLocationInput.module.css +++ /dev/null @@ -1,64 +0,0 @@ -.locationInputContainer { - width: 100%; - margin-bottom: 1.5rem; -} - -.fieldsRow { - display: flex; - gap: 1rem; - align-items: flex-end; -} - -.fieldWrapper { - flex: 1; -} - -.buttonsWrapper { - display: flex; - flex-direction: row; - gap: 0.5rem; - min-width: 150px; -} - -.searchButton { - white-space: nowrap; -} - -.locationButton { - white-space: nowrap; -} - -@media (max-width: 1024px) { - .fieldsRow { - flex-wrap: wrap; - } - - .buttonsWrapper { - width: 100%; - } - - .fieldWrapper { - min-width: calc(50% - 0.5rem); - } -} - -@media (max-width: 768px) { - .fieldsRow { - flex-direction: column; - } - - .fieldWrapper { - width: 100%; - min-width: 100%; - } - - .buttonsWrapper { - width: 100%; - } - - .searchButton, - .locationButton { - flex: 1; - } -} - diff --git a/src/core/PageManager/data/pages/pek/PekLocationInput.tsx b/src/core/PageManager/data/pages/pek/PekLocationInput.tsx deleted file mode 100644 index ffbf062..0000000 --- a/src/core/PageManager/data/pages/pek/PekLocationInput.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React from 'react'; -import { TextField, Button } from '../../../../../components/UiComponents'; -import { FaLocationArrow } from 'react-icons/fa'; -import { IoMdSend } from 'react-icons/io'; -import { usePekContext } from '../../../../../contexts/PekContext'; -import styles from './PekLocationInput.module.css'; - -const PekLocationInput: React.FC = () => { - const { - kanton: _kanton, - setKanton: _setKanton, - gemeinde: _gemeinde, - setGemeinde: _setGemeinde, - adresse, - setAdresse, - buildLocationString, - useCurrentLocation, - isGettingLocation, - locationError: _locationError, - searchParcel, - isSearchingParcel - } = usePekContext(); - - const handleSearch = async () => { - const locationString = buildLocationString(); - if (locationString.trim()) { - await searchParcel(locationString.trim(), true); - } - }; - - const handleUseCurrentLocation = async () => { - await useCurrentLocation(); - }; - - - return ( -
    -
    -
    - { - if (e.key === 'Enter') { - e.preventDefault(); - handleSearch(); - } - }} - /> -
    -
    - - -
    -
    -
    - ); -}; - -export default PekLocationInput; - diff --git a/src/core/PageManager/data/pages/pek/PekMapView.tsx b/src/core/PageManager/data/pages/pek/PekMapView.tsx deleted file mode 100644 index 8f1035e..0000000 --- a/src/core/PageManager/data/pages/pek/PekMapView.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; -import { MapView, ParcelInfoPanel } from '../../../../../components/UiComponents'; -import { usePekContext } from '../../../../../contexts/PekContext'; - -const PekMapView: React.FC = () => { - const { - mapCenter, - mapZoomBounds, - parcelGeometries, - handleMapClick, - handleParcelClick, - selectedParcels, - removeParcel, - isPanelOpen, - setIsPanelOpen - } = usePekContext(); - - // Aggregate all adjacent parcels from all selected parcels - const allAdjacentParcels = React.useMemo(() => { - const adjacentSet = new Map(); - selectedParcels.forEach(parcel => { - if (parcel.adjacent_parcels) { - parcel.adjacent_parcels.forEach((adj: { id: string }) => { - if (!adjacentSet.has(adj.id)) { - adjacentSet.set(adj.id, adj); - } - }); - } - }); - return Array.from(adjacentSet.values()); - }, [selectedParcels]); - - return ( - <> -
    - -
    - - setIsPanelOpen(false)} - parcels={selectedParcels} - onRemoveParcel={removeParcel} - adjacentParcels={allAdjacentParcels} - /> - - ); -}; - -export default PekMapView; - diff --git a/src/core/PageManager/data/pages/pek/PekPageWrapper.tsx b/src/core/PageManager/data/pages/pek/PekPageWrapper.tsx deleted file mode 100644 index 48f20f9..0000000 --- a/src/core/PageManager/data/pages/pek/PekPageWrapper.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { PekProvider } from '../../../../../contexts/PekContext'; -import PageRenderer from '../../../PageRenderer'; -import { pekPageData } from '../pek'; - -const PekPageWrapper: React.FC = () => { - // Create a version of pageData without customComponent to avoid infinite loop - const { customComponent, ...pageDataWithoutCustom } = pekPageData; - - return ( - - - - ); -}; - -export default PekPageWrapper; - diff --git a/src/core/PageManager/data/pages/prompts.ts b/src/core/PageManager/data/pages/prompts.ts deleted file mode 100644 index 87e37c9..0000000 --- a/src/core/PageManager/data/pages/prompts.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { useCallback } from 'react'; -import { GenericPageData } from '../../pageInterface'; -import { FaLightbulb, FaPlus } from 'react-icons/fa'; -import { usePrompts, usePromptOperations } from '../../../../hooks/usePrompts'; - -// Helper function to convert attribute definitions to column config -const attributesToColumns = (attributes: any[]) => { - return attributes.map(attr => { - // Check if this is a date/timestamp field - disable filtering for these - const isDateField = attr.type === 'date' || - /(date|Date|timestamp|Timestamp|created|Created|updated|Updated|at|At)$/i.test(attr.name); - - return { - key: attr.name, - label: attr.label || attr.name, - type: attr.type || 'string', - width: attr.width || 200, - minWidth: attr.minWidth || 100, - maxWidth: attr.maxWidth || 400, - sortable: attr.sortable !== false, - // Disable filtering for date/timestamp fields - filterable: isDateField ? false : (attr.filterable !== false), - searchable: attr.searchable !== false, - filterOptions: attr.filterOptions - }; - }); -}; - -// Hook factory function for prompts data -const createPromptsHook = () => { - return () => { - const { - prompts, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - attributes, - permissions, - pagination, - fetchPromptById, - generateEditFieldsFromAttributes, - generateCreateFieldsFromAttributes, - ensureAttributesLoaded - } = usePrompts(); - const { - handlePromptDelete, - handlePromptCreate, - handlePromptUpdate, - handleInlineUpdate, - deletingPrompts, - creatingPrompt, - deleteError, - createError, - updateError - } = usePromptOperations(); - - const generatedColumns = attributes && attributes.length > 0 - ? attributesToColumns(attributes) - : undefined; - - // Wrapped create handler that adds mandateId from currentUser cache - const wrappedHandlePromptCreate = useCallback(async (formData: { name: string; content: string }) => { - // mandateId is retrieved inside handlePromptCreate from sessionStorage cache - return await handlePromptCreate({ name: formData.name, content: formData.content }); - }, [handlePromptCreate]); - - // Handle single prompt deletion for FormGenerator - const handleDeleteSingle = useCallback(async (prompt: any) => { - const success = await handlePromptDelete(prompt.id); - - if (success) { - refetch(); - } - }, [handlePromptDelete, refetch]); - - // Handle multiple prompt deletion for FormGenerator - const handleDeleteMultiple = useCallback(async (selectedPrompts: any[]) => { - const promptIds = selectedPrompts.map(prompt => prompt.id); - const results = await Promise.all( - promptIds.map(id => handlePromptDelete(id)) - ); - - const allSuccessful = results.every(result => result); - - if (allSuccessful) { - refetch(); - } - }, [handlePromptDelete, refetch]); - - return { - data: prompts, - loading, - error, - refetch, - removeOptimistically, // Expose optimistic removal (generic name for DeleteActionButton) - updateOptimistically, // Expose optimistic update for EditActionButton - // Operations - handleDelete: handlePromptDelete, - handleDeleteMultiple, - handlePromptCreate: wrappedHandlePromptCreate, // Use wrapped version - handlePromptUpdate, - handleInlineUpdate, // For inline boolean editing in table - // FormGenerator specific handlers - onDelete: handleDeleteSingle, - onDeleteMultiple: handleDeleteMultiple, - // Loading states - deletingPrompts, - creatingPrompt, - // Error states - deleteError, - createError, - updateError, - // Attributes and permissions for dynamic column/button generation - attributes, - permissions, - pagination, // Pagination metadata from backend - columns: generatedColumns, // Return generated columns - // Functions for EditActionButton - fetchPromptById, // Fetch single prompt by ID - generateEditFieldsFromAttributes, // Generate edit fields from attributes - generateCreateFieldsFromAttributes, // Generate create fields from attributes - ensureAttributesLoaded // Generic function to ensure attributes are loaded - }; - }; -}; - - -export const promptsPageData: GenericPageData = { - id: 'basedata-prompts', - path: 'basedata/prompts', - name: 'prompts.title', - description: 'prompts.description', - - // Parent page - now under 'basedata' group (formerly 'administration') - parentPath: 'basedata', - - // Visual - icon: FaLightbulb, - title: 'prompts.title', - subtitle: 'prompts.subtitle', - - // Header buttons - headerButtons: [ - { - id: 'new-prompt', - label: 'prompts.new_button', - icon: FaPlus, - variant: 'primary', - formConfig: { - // Fields will be generated dynamically from attributes via generateCreateFieldsFromAttributes - // PageRenderer will use generateCreateFieldsFromAttributes if available, otherwise generateEditFieldsFromAttributes - fields: [], // Empty array - fields will be generated dynamically from attributes - popupTitle: 'prompts.modal.create.title', - popupSize: 'medium', - createOperationName: 'handlePromptCreate', - successMessage: 'prompts.create.success', - errorMessage: 'prompts.create.error' - } - } - ], - - // Content sections - using generic table approach - content: [ - { - id: 'prompts-table', - type: 'table', - tableConfig: { - hookFactory: createPromptsHook, - // Columns are generated dynamically from attributes via hookData.columns - actionButtons: [ - { - type: 'play', - title: 'prompts.action.start', - idField: 'id', - nameField: 'name', - contentField: 'content', - mode: 'prompt', - navigateTo: 'start/dashboard', - // Only show if user has read permission (permissions.read !== 'n') - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasRead = hookData.permissions.read !== 'n' && hookData.permissions.view; - return { disabled: !hasRead, message: 'No permission to start prompts' }; - } - }, - { - type: 'edit', - title: 'prompts.action.edit', - idField: 'id', - nameField: 'name', - operationName: 'handlePromptUpdate', - loadingStateName: 'updatingPrompts', - fetchItemFunctionName: 'fetchPromptById', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view; - return { disabled: !hasUpdate, message: 'No permission to edit prompts' }; - } - }, - { - type: 'delete', - title: 'prompts.action.delete', - idField: 'id', - operationName: 'handleDelete', - loadingStateName: 'deletingPrompts', // Entity-specific loading state - // Only show if user has delete permission (permissions.delete !== 'n') - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view; - return { disabled: !hasDelete, message: 'No permission to delete prompts' }; - } - }, - { - type: 'copy', - title: 'prompts.action.copy', - idField: 'id', - nameField: 'name', - contentField: 'content', - operationName: 'handlePromptCreate', - loadingStateName: 'creatingPrompt', - // Only show if user has create permission (permissions.create !== 'n') - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasCreate = hookData.permissions.create !== 'n' && hookData.permissions.view; - return { disabled: !hasCreate, message: 'No permission to create prompts' }; - } - } - ], - searchable: true, - filterable: true, - sortable: true, - resizable: true, - pagination: true, - pageSize: 10, - className: 'prompts-table' - } - } - ], - - // Page behavior - persistent: false, - preload: false, - preserveState: true, // Keep page mounted and prevent refetching - moduleEnabled: true, - - // shown in sidebar under administration - - // No drag and drop configuration for prompts - - // Lifecycle hooks - onActivate: async () => { - if (import.meta.env.DEV) console.log('Prompts activated'); - }, - onLoad: async () => { - if (import.meta.env.DEV) console.log('Prompts loaded - can initialize prompts list here'); - }, - onUnload: async () => { - if (import.meta.env.DEV) console.log('Prompts unloaded - cleanup prompts references'); - } -}; - diff --git a/src/core/PageManager/data/pages/settings.ts b/src/core/PageManager/data/pages/settings.ts deleted file mode 100644 index c4c7925..0000000 --- a/src/core/PageManager/data/pages/settings.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { GenericPageData } from '../../pageInterface'; -import { FaCog } from 'react-icons/fa'; -import { createSettingsHook } from '../../../../hooks/useSettings'; - -export const settingsPageData: GenericPageData = { - id: 'settings', - path: 'settings', - name: 'Settings', - description: 'Manage your account settings and preferences', - - // Visual - icon: FaCog, - title: 'settings.title', - subtitle: 'Manage your account settings and preferences', - - // Page behavior - persistent: false, - preserveState: false, - preload: true, - moduleEnabled: true, - - // Sidebar - showInSidebar: true, - order: 999, // Put at the end - - // Content sections - content: [ - { - id: 'settings-content', - type: 'settings', - settingsConfig: { - sections: [ - { - id: 'user-info', - sectionId: 'user-info', - title: 'settings.userinfo', - description: 'settings.userinfo.description', - saveButtonLabel: 'settings.userinfo.save', - saveButtonVariant: 'primary', - saveButtonSize: 'md', - // Static fields for user information - staticFields: [ - { - id: 'username', - type: 'text', - label: 'settings.userinfo.username', - description: 'settings.userinfo.username.description', - dataKey: 'username', - required: true, - placeholder: 'settings.userinfo.username' - }, - { - id: 'fullName', - type: 'text', - label: 'settings.userinfo.fullname', - description: 'settings.userinfo.fullname.description', - dataKey: 'fullName', - required: false, - placeholder: 'settings.userinfo.fullname' - }, - { - id: 'email', - type: 'text', - inputType: 'email', - label: 'settings.userinfo.email', - description: 'settings.userinfo.email.description', - dataKey: 'email', - required: true, - placeholder: 'settings.userinfo.email' - }, - { - id: 'language', - type: 'select', - label: 'settings.userinfo.language', - description: 'settings.userinfo.language.description', - dataKey: 'language', - required: false, - options: [ - { id: 'en', label: 'English', value: 'en' }, - { id: 'de', label: 'Deutsch', value: 'de' }, - { id: 'fr', label: 'Français', value: 'fr' }, - { id: 'it', label: 'Italiano', value: 'it' } - ] - }, - { - id: 'phone-name', - type: 'text', - label: 'settings.userinfo.phone_name', - description: 'settings.userinfo.phone_name.description', - dataKey: 'phoneName', - required: false, - placeholder: 'settings.userinfo.phone_name' - } - ] - }, - { - id: 'theme', - title: '', - sectionId: 'theme', - saveButtonLabel: 'settings.save', - saveButtonVariant: 'primary', - saveButtonSize: 'md', - staticFields: [ - { - id: 'theme-select', - type: 'select', - label: 'settings.theme', - dataKey: 'theme', - required: false, - options: [ - { id: 'light', label: 'settings.theme.light', value: 'light' }, - { id: 'dark', label: 'settings.theme.dark', value: 'dark' } - ] - } - ] - } - ], - hookFactory: createSettingsHook - } - } - ], - - // Lifecycle hooks - onActivate: async () => { - if (import.meta.env.DEV) console.log('Settings page activated'); - } -}; - diff --git a/src/core/PageManager/data/pages/trustee/access.ts b/src/core/PageManager/data/pages/trustee/access.ts deleted file mode 100644 index 52fe0c9..0000000 --- a/src/core/PageManager/data/pages/trustee/access.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { useCallback } from 'react'; -import { GenericPageData } from '../../../pageInterface'; -import { FaKey, FaPlus } from 'react-icons/fa'; -import { useTrusteeAccess, useTrusteeAccessOperations } from '../../../../../hooks/useTrustee'; - -const attributesToColumns = (attributes: any[]) => { - return attributes.map(attr => { - const isDateField = attr.type === 'date' || attr.type === 'timestamp' || - /(date|Date|timestamp|Timestamp|created|Created|updated|Updated|at|At)$/i.test(attr.name); - - return { - key: attr.name, - label: attr.label || attr.name, - type: attr.type || 'string', - width: attr.width || 200, - minWidth: attr.minWidth || 100, - maxWidth: attr.maxWidth || 400, - sortable: attr.sortable !== false, - filterable: isDateField ? false : (attr.filterable !== false), - searchable: attr.searchable !== false, - filterOptions: attr.filterOptions - }; - }); -}; - -const createAccessHook = () => { - return () => { - const { - items: accessRecords, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - attributes, - permissions, - fetchById, - generateEditFieldsFromAttributes, - generateCreateFieldsFromAttributes, - ensureAttributesLoaded - } = useTrusteeAccess(); - const { - handleDelete, - handleCreate, - handleUpdate, - deletingItems, - creatingItem, - deleteError, - createError, - updateError - } = useTrusteeAccessOperations(); - - const generatedColumns = attributes && attributes.length > 0 - ? attributesToColumns(attributes) - : undefined; - - const wrappedHandleCreate = useCallback(async (formData: any) => { - return await handleCreate(formData); - }, [handleCreate]); - - const handleDeleteSingle = useCallback(async (item: any) => { - const success = await handleDelete(item.id); - if (success) { - refetch(); - } - }, [handleDelete, refetch]); - - const handleDeleteMultiple = useCallback(async (selectedItems: any[]) => { - const ids = selectedItems.map(item => item.id); - const results = await Promise.all(ids.map(id => handleDelete(id))); - const allSuccessful = results.every(result => result); - if (allSuccessful) { - refetch(); - } - }, [handleDelete, refetch]); - - return { - data: accessRecords, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - handleDelete, - handleDeleteMultiple, - handleCreate: wrappedHandleCreate, - handleUpdate, - onDelete: handleDeleteSingle, - onDeleteMultiple: handleDeleteMultiple, - deletingItems, - creatingItem, - deleteError, - createError, - updateError, - attributes, - permissions, - columns: generatedColumns, - fetchById, - generateEditFieldsFromAttributes, - generateCreateFieldsFromAttributes, - ensureAttributesLoaded - }; - }; -}; - -export const trusteeAccessPageData: GenericPageData = { - id: 'trustee-access', - path: 'trustee/access', - name: 'trustee.access.title', - description: 'trustee.access.description', - parentPath: 'start.trustee', - - icon: FaKey, - title: 'trustee.access.title', - subtitle: 'trustee.access.subtitle', - - headerButtons: [ - { - id: 'new-access', - label: 'trustee.access.new_button', - icon: FaPlus, - variant: 'primary', - formConfig: { - fields: [ - { - key: 'organisationId', - label: 'trustee.access.field.organisationId', - type: 'enum', - required: true, - optionsReference: 'TrusteeOrganisation' - }, - { - key: 'roleId', - label: 'trustee.access.field.roleId', - type: 'enum', - required: true, - optionsReference: 'TrusteeRole' - }, - { - key: 'userId', - label: 'trustee.access.field.userId', - type: 'enum', - required: true, - optionsReference: 'User' - }, - { - key: 'contractId', - label: 'trustee.access.field.contractId', - type: 'enum', - required: false, - optionsReference: 'TrusteeContract', - placeholder: 'trustee.access.field.contractId_placeholder' - } - ], - popupTitle: 'trustee.access.modal.create.title', - popupSize: 'medium', - createOperationName: 'handleCreate', - successMessage: 'trustee.access.create.success', - errorMessage: 'trustee.access.create.error' - } - } - ], - - content: [ - { - id: 'access-table', - type: 'table', - tableConfig: { - hookFactory: createAccessHook, - actionButtons: [ - { - type: 'edit', - title: 'trustee.access.action.edit', - idField: 'id', - nameField: 'id', - operationName: 'handleUpdate', - loadingStateName: 'updatingItems', - fetchItemFunctionName: 'fetchById', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view; - return { disabled: !hasUpdate, message: 'No permission to edit access' }; - } - }, - { - type: 'delete', - title: 'trustee.access.action.delete', - idField: 'id', - operationName: 'handleDelete', - loadingStateName: 'deletingItems', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view; - return { disabled: !hasDelete, message: 'No permission to delete access' }; - } - } - ], - searchable: true, - filterable: true, - sortable: true, - resizable: true, - pagination: true, - pageSize: 10, - className: 'trustee-access-table' - } - } - ], - - persistent: false, - preload: false, - preserveState: true, - moduleEnabled: true, - - onActivate: async () => { - if (import.meta.env.DEV) console.log('Trustee Access activated'); - }, - onLoad: async () => { - if (import.meta.env.DEV) console.log('Trustee Access loaded'); - }, - onUnload: async () => { - if (import.meta.env.DEV) console.log('Trustee Access unloaded'); - } -}; diff --git a/src/core/PageManager/data/pages/trustee/contracts.ts b/src/core/PageManager/data/pages/trustee/contracts.ts deleted file mode 100644 index 4078b0e..0000000 --- a/src/core/PageManager/data/pages/trustee/contracts.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { useCallback } from 'react'; -import { GenericPageData } from '../../../pageInterface'; -import { FaFileContract, FaPlus } from 'react-icons/fa'; -import { useTrusteeContracts, useTrusteeContractOperations } from '../../../../../hooks/useTrustee'; - -const attributesToColumns = (attributes: any[]) => { - return attributes.map(attr => { - const isDateField = attr.type === 'date' || attr.type === 'timestamp' || - /(date|Date|timestamp|Timestamp|created|Created|updated|Updated|at|At)$/i.test(attr.name); - - return { - key: attr.name, - label: attr.label || attr.name, - type: attr.type || 'string', - width: attr.width || 200, - minWidth: attr.minWidth || 100, - maxWidth: attr.maxWidth || 400, - sortable: attr.sortable !== false, - filterable: isDateField ? false : (attr.filterable !== false), - searchable: attr.searchable !== false, - filterOptions: attr.filterOptions - }; - }); -}; - -const createContractsHook = () => { - return () => { - const { - items: contracts, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - attributes, - permissions, - fetchById, - generateEditFieldsFromAttributes, - generateCreateFieldsFromAttributes, - ensureAttributesLoaded - } = useTrusteeContracts(); - const { - handleDelete, - handleCreate, - handleUpdate, - deletingItems, - creatingItem, - deleteError, - createError, - updateError - } = useTrusteeContractOperations(); - - const generatedColumns = attributes && attributes.length > 0 - ? attributesToColumns(attributes) - : undefined; - - const wrappedHandleCreate = useCallback(async (formData: any) => { - return await handleCreate(formData); - }, [handleCreate]); - - const handleDeleteSingle = useCallback(async (item: any) => { - const success = await handleDelete(item.id); - if (success) { - refetch(); - } - }, [handleDelete, refetch]); - - const handleDeleteMultiple = useCallback(async (selectedItems: any[]) => { - const ids = selectedItems.map(item => item.id); - const results = await Promise.all(ids.map(id => handleDelete(id))); - const allSuccessful = results.every(result => result); - if (allSuccessful) { - refetch(); - } - }, [handleDelete, refetch]); - - return { - data: contracts, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - handleDelete, - handleDeleteMultiple, - handleCreate: wrappedHandleCreate, - handleUpdate, - onDelete: handleDeleteSingle, - onDeleteMultiple: handleDeleteMultiple, - deletingItems, - creatingItem, - deleteError, - createError, - updateError, - attributes, - permissions, - columns: generatedColumns, - fetchById, - generateEditFieldsFromAttributes, - generateCreateFieldsFromAttributes, - ensureAttributesLoaded - }; - }; -}; - -export const trusteeContractsPageData: GenericPageData = { - id: 'trustee-contracts', - path: 'trustee/contracts', - name: 'trustee.contracts.title', - description: 'trustee.contracts.description', - parentPath: 'start.trustee', - - icon: FaFileContract, - title: 'trustee.contracts.title', - subtitle: 'trustee.contracts.subtitle', - - headerButtons: [ - { - id: 'new-contract', - label: 'trustee.contracts.new_button', - icon: FaPlus, - variant: 'primary', - formConfig: { - fields: [ - { - key: 'organisationId', - label: 'trustee.contracts.field.organisationId', - type: 'enum', - required: true, - optionsReference: 'TrusteeOrganisation' - }, - { - key: 'label', - label: 'trustee.contracts.field.label', - type: 'string', - required: true, - placeholder: 'trustee.contracts.field.label_placeholder' - }, - { - key: 'enabled', - label: 'trustee.contracts.field.enabled', - type: 'boolean', - required: false - } - ], - popupTitle: 'trustee.contracts.modal.create.title', - popupSize: 'medium', - createOperationName: 'handleCreate', - successMessage: 'trustee.contracts.create.success', - errorMessage: 'trustee.contracts.create.error' - } - } - ], - - content: [ - { - id: 'contracts-table', - type: 'table', - tableConfig: { - hookFactory: createContractsHook, - actionButtons: [ - { - type: 'edit', - title: 'trustee.contracts.action.edit', - idField: 'id', - nameField: 'label', - operationName: 'handleUpdate', - loadingStateName: 'updatingItems', - fetchItemFunctionName: 'fetchById', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view; - return { disabled: !hasUpdate, message: 'No permission to edit contracts' }; - } - }, - { - type: 'delete', - title: 'trustee.contracts.action.delete', - idField: 'id', - operationName: 'handleDelete', - loadingStateName: 'deletingItems', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view; - return { disabled: !hasDelete, message: 'No permission to delete contracts' }; - } - } - ], - searchable: true, - filterable: true, - sortable: true, - resizable: true, - pagination: true, - pageSize: 10, - className: 'trustee-contracts-table' - } - } - ], - - persistent: false, - preload: false, - preserveState: true, - moduleEnabled: true, - - onActivate: async () => { - if (import.meta.env.DEV) console.log('Trustee Contracts activated'); - }, - onLoad: async () => { - if (import.meta.env.DEV) console.log('Trustee Contracts loaded'); - }, - onUnload: async () => { - if (import.meta.env.DEV) console.log('Trustee Contracts unloaded'); - } -}; diff --git a/src/core/PageManager/data/pages/trustee/documents.ts b/src/core/PageManager/data/pages/trustee/documents.ts deleted file mode 100644 index ab8a97d..0000000 --- a/src/core/PageManager/data/pages/trustee/documents.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { useCallback } from 'react'; -import { GenericPageData } from '../../../pageInterface'; -import { FaFile, FaPlus } from 'react-icons/fa'; -import { useTrusteeDocuments, useTrusteeDocumentOperations } from '../../../../../hooks/useTrustee'; - -const attributesToColumns = (attributes: any[]) => { - return attributes.map(attr => { - const isDateField = attr.type === 'date' || attr.type === 'timestamp' || - /(date|Date|timestamp|Timestamp|created|Created|updated|Updated|at|At)$/i.test(attr.name); - - // Hide binary data column - if (attr.name === 'documentData') { - return null; - } - - return { - key: attr.name, - label: attr.label || attr.name, - type: attr.type || 'string', - width: attr.width || 200, - minWidth: attr.minWidth || 100, - maxWidth: attr.maxWidth || 400, - sortable: attr.sortable !== false, - filterable: isDateField ? false : (attr.filterable !== false), - searchable: attr.searchable !== false, - filterOptions: attr.filterOptions - }; - }).filter(Boolean); -}; - -const createDocumentsHook = () => { - return () => { - const { - items: documents, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - attributes, - permissions, - fetchById, - generateEditFieldsFromAttributes, - generateCreateFieldsFromAttributes, - ensureAttributesLoaded - } = useTrusteeDocuments(); - const { - handleDelete, - handleCreate, - handleUpdate, - deletingItems, - creatingItem, - deleteError, - createError, - updateError - } = useTrusteeDocumentOperations(); - - const generatedColumns = attributes && attributes.length > 0 - ? attributesToColumns(attributes) - : undefined; - - const wrappedHandleCreate = useCallback(async (formData: any) => { - return await handleCreate(formData); - }, [handleCreate]); - - const handleDeleteSingle = useCallback(async (item: any) => { - const success = await handleDelete(item.id); - if (success) { - refetch(); - } - }, [handleDelete, refetch]); - - const handleDeleteMultiple = useCallback(async (selectedItems: any[]) => { - const ids = selectedItems.map(item => item.id); - const results = await Promise.all(ids.map(id => handleDelete(id))); - const allSuccessful = results.every(result => result); - if (allSuccessful) { - refetch(); - } - }, [handleDelete, refetch]); - - return { - data: documents, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - handleDelete, - handleDeleteMultiple, - handleCreate: wrappedHandleCreate, - handleUpdate, - onDelete: handleDeleteSingle, - onDeleteMultiple: handleDeleteMultiple, - deletingItems, - creatingItem, - deleteError, - createError, - updateError, - attributes, - permissions, - columns: generatedColumns, - fetchById, - generateEditFieldsFromAttributes, - generateCreateFieldsFromAttributes, - ensureAttributesLoaded - }; - }; -}; - -export const trusteeDocumentsPageData: GenericPageData = { - id: 'trustee-documents', - path: 'trustee/documents', - name: 'trustee.documents.title', - description: 'trustee.documents.description', - parentPath: 'start.trustee', - - icon: FaFile, - title: 'trustee.documents.title', - subtitle: 'trustee.documents.subtitle', - - headerButtons: [ - { - id: 'new-document', - label: 'trustee.documents.new_button', - icon: FaPlus, - variant: 'primary', - formConfig: { - fields: [ - { - key: 'organisationId', - label: 'trustee.documents.field.organisationId', - type: 'enum', - required: true, - optionsReference: 'TrusteeOrganisation' - }, - { - key: 'contractId', - label: 'trustee.documents.field.contractId', - type: 'enum', - required: true, - optionsReference: 'TrusteeContract' - }, - { - key: 'documentName', - label: 'trustee.documents.field.documentName', - type: 'string', - required: true, - placeholder: 'trustee.documents.field.documentName_placeholder' - }, - { - key: 'documentMimeType', - label: 'trustee.documents.field.documentMimeType', - type: 'enum', - required: true, - options: [ - { value: 'application/pdf', label: 'PDF' }, - { value: 'image/jpeg', label: 'JPEG' }, - { value: 'image/png', label: 'PNG' }, - { value: 'application/octet-stream', label: 'Other' } - ] - } - ], - popupTitle: 'trustee.documents.modal.create.title', - popupSize: 'medium', - createOperationName: 'handleCreate', - successMessage: 'trustee.documents.create.success', - errorMessage: 'trustee.documents.create.error' - } - } - ], - - content: [ - { - id: 'documents-table', - type: 'table', - tableConfig: { - hookFactory: createDocumentsHook, - actionButtons: [ - { - type: 'edit', - title: 'trustee.documents.action.edit', - idField: 'id', - nameField: 'documentName', - operationName: 'handleUpdate', - loadingStateName: 'updatingItems', - fetchItemFunctionName: 'fetchById', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view; - return { disabled: !hasUpdate, message: 'No permission to edit documents' }; - } - }, - { - type: 'delete', - title: 'trustee.documents.action.delete', - idField: 'id', - operationName: 'handleDelete', - loadingStateName: 'deletingItems', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view; - return { disabled: !hasDelete, message: 'No permission to delete documents' }; - } - } - ], - searchable: true, - filterable: true, - sortable: true, - resizable: true, - pagination: true, - pageSize: 10, - className: 'trustee-documents-table' - } - } - ], - - persistent: false, - preload: false, - preserveState: true, - moduleEnabled: true, - - onActivate: async () => { - if (import.meta.env.DEV) console.log('Trustee Documents activated'); - }, - onLoad: async () => { - if (import.meta.env.DEV) console.log('Trustee Documents loaded'); - }, - onUnload: async () => { - if (import.meta.env.DEV) console.log('Trustee Documents unloaded'); - } -}; diff --git a/src/core/PageManager/data/pages/trustee/index.ts b/src/core/PageManager/data/pages/trustee/index.ts deleted file mode 100644 index 6632ccc..0000000 --- a/src/core/PageManager/data/pages/trustee/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { GenericPageData } from '../../../pageInterface'; - -// Import all trustee page configurations -import { trusteeOrganisationsPageData } from './organisations'; -import { trusteeRolesPageData } from './roles'; -import { trusteeAccessPageData } from './access'; -import { trusteeContractsPageData } from './contracts'; -import { trusteeDocumentsPageData } from './documents'; -import { trusteePositionsPageData } from './positions'; - -// Export all trustee pages -export { - trusteeOrganisationsPageData, - trusteeRolesPageData, - trusteeAccessPageData, - trusteeContractsPageData, - trusteeDocumentsPageData, - trusteePositionsPageData -}; - -// Export array of all trustee pages for registration -// No explicit container needed - SidebarProvider creates a virtual parent group -// based on parentPath: 'trustee' in the child pages -export const trusteePages: GenericPageData[] = [ - trusteeOrganisationsPageData, - trusteeRolesPageData, - trusteeAccessPageData, - trusteeContractsPageData, - trusteeDocumentsPageData, - trusteePositionsPageData -]; diff --git a/src/core/PageManager/data/pages/trustee/organisations.ts b/src/core/PageManager/data/pages/trustee/organisations.ts deleted file mode 100644 index a5f4a94..0000000 --- a/src/core/PageManager/data/pages/trustee/organisations.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { useCallback } from 'react'; -import { GenericPageData } from '../../../pageInterface'; -import { FaBuilding, FaPlus } from 'react-icons/fa'; -import { useTrusteeOrganisations, useTrusteeOrganisationOperations } from '../../../../../hooks/useTrustee'; - -// Helper function to convert attribute definitions to column config -const attributesToColumns = (attributes: any[]) => { - return attributes.map(attr => { - const isDateField = attr.type === 'date' || attr.type === 'timestamp' || - /(date|Date|timestamp|Timestamp|created|Created|updated|Updated|at|At)$/i.test(attr.name); - - return { - key: attr.name, - label: attr.label || attr.name, - type: attr.type || 'string', - width: attr.width || 200, - minWidth: attr.minWidth || 100, - maxWidth: attr.maxWidth || 400, - sortable: attr.sortable !== false, - filterable: isDateField ? false : (attr.filterable !== false), - searchable: attr.searchable !== false, - filterOptions: attr.filterOptions - }; - }); -}; - -// Hook factory function for organisations data -const createOrganisationsHook = () => { - return () => { - const { - items: organisations, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - attributes, - permissions, - fetchById, - generateEditFieldsFromAttributes, - generateCreateFieldsFromAttributes, - ensureAttributesLoaded - } = useTrusteeOrganisations(); - const { - handleDelete, - handleCreate, - handleUpdate, - deletingItems, - creatingItem, - deleteError, - createError, - updateError - } = useTrusteeOrganisationOperations(); - - const generatedColumns = attributes && attributes.length > 0 - ? attributesToColumns(attributes) - : undefined; - - const wrappedHandleCreate = useCallback(async (formData: any) => { - return await handleCreate(formData); - }, [handleCreate]); - - const handleDeleteSingle = useCallback(async (item: any) => { - const success = await handleDelete(item.id); - if (success) { - refetch(); - } - }, [handleDelete, refetch]); - - const handleDeleteMultiple = useCallback(async (selectedItems: any[]) => { - const ids = selectedItems.map(item => item.id); - const results = await Promise.all(ids.map(id => handleDelete(id))); - const allSuccessful = results.every(result => result); - if (allSuccessful) { - refetch(); - } - }, [handleDelete, refetch]); - - return { - data: organisations, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - handleDelete, - handleDeleteMultiple, - handleCreate: wrappedHandleCreate, - handleUpdate, - onDelete: handleDeleteSingle, - onDeleteMultiple: handleDeleteMultiple, - deletingItems, - creatingItem, - deleteError, - createError, - updateError, - attributes, - permissions, - columns: generatedColumns, - fetchById, - generateEditFieldsFromAttributes, - generateCreateFieldsFromAttributes, - ensureAttributesLoaded - }; - }; -}; - -export const trusteeOrganisationsPageData: GenericPageData = { - id: 'trustee-organisations', - path: 'trustee/organisations', - name: 'trustee.organisations.title', - description: 'trustee.organisations.description', - parentPath: 'start.trustee', - - icon: FaBuilding, - title: 'trustee.organisations.title', - subtitle: 'trustee.organisations.subtitle', - - headerButtons: [ - { - id: 'new-organisation', - label: 'trustee.organisations.new_button', - icon: FaPlus, - variant: 'primary', - formConfig: { - fields: [ - { - key: 'id', - label: 'trustee.organisations.field.id', - type: 'string', - required: true, - placeholder: 'trustee.organisations.field.id_placeholder', - validator: (value: string) => { - if (!value || value.trim() === '') { - return 'Organisation ID cannot be empty'; - } - if (value.length < 3 || value.length > 50) { - return 'Organisation ID must be 3-50 characters'; - } - if (!/^[a-zA-Z0-9_-]+$/.test(value)) { - return 'Organisation ID can only contain letters, numbers, hyphens, and underscores'; - } - return null; - } - }, - { - key: 'label', - label: 'trustee.organisations.field.label', - type: 'string', - required: true, - placeholder: 'trustee.organisations.field.label_placeholder' - }, - { - key: 'enabled', - label: 'trustee.organisations.field.enabled', - type: 'boolean', - required: false - } - ], - popupTitle: 'trustee.organisations.modal.create.title', - popupSize: 'medium', - createOperationName: 'handleCreate', - successMessage: 'trustee.organisations.create.success', - errorMessage: 'trustee.organisations.create.error' - } - } - ], - - content: [ - { - id: 'organisations-table', - type: 'table', - tableConfig: { - hookFactory: createOrganisationsHook, - actionButtons: [ - { - type: 'edit', - title: 'trustee.organisations.action.edit', - idField: 'id', - nameField: 'label', - operationName: 'handleUpdate', - loadingStateName: 'updatingItems', - fetchItemFunctionName: 'fetchById', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view; - return { disabled: !hasUpdate, message: 'No permission to edit organisations' }; - } - }, - { - type: 'delete', - title: 'trustee.organisations.action.delete', - idField: 'id', - operationName: 'handleDelete', - loadingStateName: 'deletingItems', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view; - return { disabled: !hasDelete, message: 'No permission to delete organisations' }; - } - } - ], - searchable: true, - filterable: true, - sortable: true, - resizable: true, - pagination: true, - pageSize: 10, - className: 'trustee-organisations-table' - } - } - ], - - persistent: false, - preload: false, - preserveState: true, - moduleEnabled: true, - - onActivate: async () => { - if (import.meta.env.DEV) console.log('Trustee Organisations activated'); - }, - onLoad: async () => { - if (import.meta.env.DEV) console.log('Trustee Organisations loaded'); - }, - onUnload: async () => { - if (import.meta.env.DEV) console.log('Trustee Organisations unloaded'); - } -}; diff --git a/src/core/PageManager/data/pages/trustee/positions.ts b/src/core/PageManager/data/pages/trustee/positions.ts deleted file mode 100644 index af49339..0000000 --- a/src/core/PageManager/data/pages/trustee/positions.ts +++ /dev/null @@ -1,281 +0,0 @@ -import { useCallback } from 'react'; -import { GenericPageData } from '../../../pageInterface'; -import { FaReceipt, FaPlus } from 'react-icons/fa'; -import { useTrusteePositions, useTrusteePositionOperations } from '../../../../../hooks/useTrustee'; - -const attributesToColumns = (attributes: any[]) => { - return attributes.map(attr => { - const isDateField = attr.type === 'date' || attr.type === 'timestamp' || - /(date|Date|timestamp|Timestamp|created|Created|updated|Updated|at|At)$/i.test(attr.name); - - return { - key: attr.name, - label: attr.label || attr.name, - type: attr.type || 'string', - width: attr.width || 200, - minWidth: attr.minWidth || 100, - maxWidth: attr.maxWidth || 400, - sortable: attr.sortable !== false, - filterable: isDateField ? false : (attr.filterable !== false), - searchable: attr.searchable !== false, - filterOptions: attr.filterOptions - }; - }); -}; - -const createPositionsHook = () => { - return () => { - const { - items: positions, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - attributes, - permissions, - fetchById, - generateEditFieldsFromAttributes, - generateCreateFieldsFromAttributes, - ensureAttributesLoaded - } = useTrusteePositions(); - const { - handleDelete, - handleCreate, - handleUpdate, - deletingItems, - creatingItem, - deleteError, - createError, - updateError - } = useTrusteePositionOperations(); - - const generatedColumns = attributes && attributes.length > 0 - ? attributesToColumns(attributes) - : undefined; - - const wrappedHandleCreate = useCallback(async (formData: any) => { - // Auto-calculate VAT amount if not provided - if (formData.bookingAmount && formData.vatPercentage && !formData.vatAmount) { - formData.vatAmount = formData.bookingAmount * formData.vatPercentage / 100; - } - return await handleCreate(formData); - }, [handleCreate]); - - const handleDeleteSingle = useCallback(async (item: any) => { - const success = await handleDelete(item.id); - if (success) { - refetch(); - } - }, [handleDelete, refetch]); - - const handleDeleteMultiple = useCallback(async (selectedItems: any[]) => { - const ids = selectedItems.map(item => item.id); - const results = await Promise.all(ids.map(id => handleDelete(id))); - const allSuccessful = results.every(result => result); - if (allSuccessful) { - refetch(); - } - }, [handleDelete, refetch]); - - return { - data: positions, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - handleDelete, - handleDeleteMultiple, - handleCreate: wrappedHandleCreate, - handleUpdate, - onDelete: handleDeleteSingle, - onDeleteMultiple: handleDeleteMultiple, - deletingItems, - creatingItem, - deleteError, - createError, - updateError, - attributes, - permissions, - columns: generatedColumns, - fetchById, - generateEditFieldsFromAttributes, - generateCreateFieldsFromAttributes, - ensureAttributesLoaded - }; - }; -}; - -export const trusteePositionsPageData: GenericPageData = { - id: 'trustee-positions', - path: 'trustee/positions', - name: 'trustee.positions.title', - description: 'trustee.positions.description', - parentPath: 'start.trustee', - - icon: FaReceipt, - title: 'trustee.positions.title', - subtitle: 'trustee.positions.subtitle', - - headerButtons: [ - { - id: 'new-position', - label: 'trustee.positions.new_button', - icon: FaPlus, - variant: 'primary', - formConfig: { - fields: [ - { - key: 'organisationId', - label: 'trustee.positions.field.organisationId', - type: 'enum', - required: true, - optionsReference: 'TrusteeOrganisation' - }, - { - key: 'contractId', - label: 'trustee.positions.field.contractId', - type: 'enum', - required: true, - optionsReference: 'TrusteeContract' - }, - { - key: 'valuta', - label: 'trustee.positions.field.valuta', - type: 'date', - required: true - }, - { - key: 'company', - label: 'trustee.positions.field.company', - type: 'string', - required: false, - placeholder: 'trustee.positions.field.company_placeholder' - }, - { - key: 'desc', - label: 'trustee.positions.field.desc', - type: 'textarea', - required: false, - minRows: 2, - maxRows: 4 - }, - { - key: 'bookingCurrency', - label: 'trustee.positions.field.bookingCurrency', - type: 'enum', - required: true, - options: [ - { value: 'CHF', label: 'CHF' }, - { value: 'EUR', label: 'EUR' }, - { value: 'USD', label: 'USD' }, - { value: 'GBP', label: 'GBP' } - ] - }, - { - key: 'bookingAmount', - label: 'trustee.positions.field.bookingAmount', - type: 'number', - required: true - }, - { - key: 'originalCurrency', - label: 'trustee.positions.field.originalCurrency', - type: 'enum', - required: true, - options: [ - { value: 'CHF', label: 'CHF' }, - { value: 'EUR', label: 'EUR' }, - { value: 'USD', label: 'USD' }, - { value: 'GBP', label: 'GBP' } - ] - }, - { - key: 'originalAmount', - label: 'trustee.positions.field.originalAmount', - type: 'number', - required: true - }, - { - key: 'vatPercentage', - label: 'trustee.positions.field.vatPercentage', - type: 'number', - required: false - }, - { - key: 'vatAmount', - label: 'trustee.positions.field.vatAmount', - type: 'number', - required: false - } - ], - popupTitle: 'trustee.positions.modal.create.title', - popupSize: 'large', - createOperationName: 'handleCreate', - successMessage: 'trustee.positions.create.success', - errorMessage: 'trustee.positions.create.error' - } - } - ], - - content: [ - { - id: 'positions-table', - type: 'table', - tableConfig: { - hookFactory: createPositionsHook, - actionButtons: [ - { - type: 'edit', - title: 'trustee.positions.action.edit', - idField: 'id', - nameField: 'desc', - operationName: 'handleUpdate', - loadingStateName: 'updatingItems', - fetchItemFunctionName: 'fetchById', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view; - return { disabled: !hasUpdate, message: 'No permission to edit positions' }; - } - }, - { - type: 'delete', - title: 'trustee.positions.action.delete', - idField: 'id', - operationName: 'handleDelete', - loadingStateName: 'deletingItems', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view; - return { disabled: !hasDelete, message: 'No permission to delete positions' }; - } - } - ], - searchable: true, - filterable: true, - sortable: true, - resizable: true, - pagination: true, - pageSize: 10, - className: 'trustee-positions-table' - } - } - ], - - persistent: false, - preload: false, - preserveState: true, - moduleEnabled: true, - - onActivate: async () => { - if (import.meta.env.DEV) console.log('Trustee Positions activated'); - }, - onLoad: async () => { - if (import.meta.env.DEV) console.log('Trustee Positions loaded'); - }, - onUnload: async () => { - if (import.meta.env.DEV) console.log('Trustee Positions unloaded'); - } -}; diff --git a/src/core/PageManager/data/pages/trustee/roles.ts b/src/core/PageManager/data/pages/trustee/roles.ts deleted file mode 100644 index 6e37d4e..0000000 --- a/src/core/PageManager/data/pages/trustee/roles.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { useCallback } from 'react'; -import { GenericPageData } from '../../../pageInterface'; -import { FaUserTag, FaPlus } from 'react-icons/fa'; -import { useTrusteeRoles, useTrusteeRoleOperations } from '../../../../../hooks/useTrustee'; - -const attributesToColumns = (attributes: any[]) => { - return attributes.map(attr => { - const isDateField = attr.type === 'date' || attr.type === 'timestamp' || - /(date|Date|timestamp|Timestamp|created|Created|updated|Updated|at|At)$/i.test(attr.name); - - return { - key: attr.name, - label: attr.label || attr.name, - type: attr.type || 'string', - width: attr.width || 200, - minWidth: attr.minWidth || 100, - maxWidth: attr.maxWidth || 400, - sortable: attr.sortable !== false, - filterable: isDateField ? false : (attr.filterable !== false), - searchable: attr.searchable !== false, - filterOptions: attr.filterOptions - }; - }); -}; - -const createRolesHook = () => { - return () => { - const { - items: roles, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - attributes, - permissions, - fetchById, - generateEditFieldsFromAttributes, - generateCreateFieldsFromAttributes, - ensureAttributesLoaded - } = useTrusteeRoles(); - const { - handleDelete, - handleCreate, - handleUpdate, - deletingItems, - creatingItem, - deleteError, - createError, - updateError - } = useTrusteeRoleOperations(); - - const generatedColumns = attributes && attributes.length > 0 - ? attributesToColumns(attributes) - : undefined; - - const wrappedHandleCreate = useCallback(async (formData: any) => { - return await handleCreate(formData); - }, [handleCreate]); - - const handleDeleteSingle = useCallback(async (item: any) => { - const success = await handleDelete(item.id); - if (success) { - refetch(); - } - }, [handleDelete, refetch]); - - const handleDeleteMultiple = useCallback(async (selectedItems: any[]) => { - const ids = selectedItems.map(item => item.id); - const results = await Promise.all(ids.map(id => handleDelete(id))); - const allSuccessful = results.every(result => result); - if (allSuccessful) { - refetch(); - } - }, [handleDelete, refetch]); - - return { - data: roles, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - handleDelete, - handleDeleteMultiple, - handleCreate: wrappedHandleCreate, - handleUpdate, - onDelete: handleDeleteSingle, - onDeleteMultiple: handleDeleteMultiple, - deletingItems, - creatingItem, - deleteError, - createError, - updateError, - attributes, - permissions, - columns: generatedColumns, - fetchById, - generateEditFieldsFromAttributes, - generateCreateFieldsFromAttributes, - ensureAttributesLoaded - }; - }; -}; - -export const trusteeRolesPageData: GenericPageData = { - id: 'trustee-roles', - path: 'trustee/roles', - name: 'trustee.roles.title', - description: 'trustee.roles.description', - parentPath: 'start.trustee', - - icon: FaUserTag, - title: 'trustee.roles.title', - subtitle: 'trustee.roles.subtitle', - - headerButtons: [ - { - id: 'new-role', - label: 'trustee.roles.new_button', - icon: FaPlus, - variant: 'primary', - formConfig: { - fields: [ - { - key: 'id', - label: 'trustee.roles.field.id', - type: 'string', - required: true, - placeholder: 'trustee.roles.field.id_placeholder' - }, - { - key: 'desc', - label: 'trustee.roles.field.desc', - type: 'textarea', - required: true, - placeholder: 'trustee.roles.field.desc_placeholder', - minRows: 3, - maxRows: 6 - } - ], - popupTitle: 'trustee.roles.modal.create.title', - popupSize: 'medium', - createOperationName: 'handleCreate', - successMessage: 'trustee.roles.create.success', - errorMessage: 'trustee.roles.create.error' - } - } - ], - - content: [ - { - id: 'roles-table', - type: 'table', - tableConfig: { - hookFactory: createRolesHook, - actionButtons: [ - { - type: 'edit', - title: 'trustee.roles.action.edit', - idField: 'id', - nameField: 'id', - operationName: 'handleUpdate', - loadingStateName: 'updatingItems', - fetchItemFunctionName: 'fetchById', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view; - return { disabled: !hasUpdate, message: 'No permission to edit roles' }; - } - }, - { - type: 'delete', - title: 'trustee.roles.action.delete', - idField: 'id', - operationName: 'handleDelete', - loadingStateName: 'deletingItems', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view; - return { disabled: !hasDelete, message: 'No permission to delete roles' }; - } - } - ], - searchable: true, - filterable: true, - sortable: true, - resizable: true, - pagination: true, - pageSize: 10, - className: 'trustee-roles-table' - } - } - ], - - persistent: false, - preload: false, - preserveState: true, - moduleEnabled: true, - - onActivate: async () => { - if (import.meta.env.DEV) console.log('Trustee Roles activated'); - }, - onLoad: async () => { - if (import.meta.env.DEV) console.log('Trustee Roles loaded'); - }, - onUnload: async () => { - if (import.meta.env.DEV) console.log('Trustee Roles unloaded'); - } -}; diff --git a/src/core/PageManager/data/pages/workflows.ts b/src/core/PageManager/data/pages/workflows.ts deleted file mode 100644 index 9065db0..0000000 --- a/src/core/PageManager/data/pages/workflows.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { useCallback } from 'react'; -import { GenericPageData } from '../../pageInterface'; -import { FaProjectDiagram } from 'react-icons/fa'; -import { useUserWorkflows, useWorkflowOperations } from '../../../../hooks/useWorkflows'; - -// Helper function to convert attribute definitions to column config -const attributesToColumns = (attributes: any[]) => { - return attributes - .filter(attr => { - // Exclude statistic, logs, messages, tasks, max steps, current action, total tasks, and total actions columns entirely - const attrNameLower = attr.name.toLowerCase(); - const excludedColumns = [ - 'statistic', 'statistics', 'stats', - 'logs', 'log', - 'messages', 'message', - 'tasks', 'task', - 'maxsteps', 'max_steps', 'max steps', 'maxstep', 'max_step', - 'currentaction', 'current_action', 'current action', - 'totaltasks', 'total_tasks', 'total tasks', - 'totalactions', 'total_actions', 'total actions', - 'expectedformats', 'expected_formats', 'expected formats', 'expectedformat', 'expected_format', 'expected format' - ]; - return !excludedColumns.includes(attrNameLower); - }) - .map(attr => { - // Check if this is a date/timestamp field - disable filtering for these - const attrNameLower = attr.name.toLowerCase(); - const isDateField = attr.type === 'date' || - /(date|Date|timestamp|Timestamp|created|Created|updated|Updated|at|At)$/i.test(attr.name) || - attrNameLower === 'lastactivity' || - attrNameLower === 'last_activity' || - attrNameLower === 'last activity'; - - const column: any = { - key: attr.name, - label: attr.label || attr.name, - type: attr.type || 'string', - width: attr.width || 200, - minWidth: attr.minWidth || 100, - maxWidth: attr.maxWidth || 400, - sortable: attr.sortable !== false, - // Disable filtering for date/timestamp fields - filterable: isDateField ? false : (attr.filterable !== false), - searchable: attr.searchable !== false, - filterOptions: attr.filterOptions - }; - - // Add explicit formatters for numeric fields that should NOT be treated as dates - if (attrNameLower === 'currentround' || attrNameLower === 'current_round' || attrNameLower === 'current round') { - column.formatter = (value: any) => { - if (value === null || value === undefined) return '-'; - return typeof value === 'number' ? value.toString() : value; - }; - // Ensure type is set to number to prevent date formatting - column.type = 'number'; - } else if (attrNameLower === 'currenttask' || attrNameLower === 'current_task' || attrNameLower === 'current task') { - column.formatter = (value: any) => { - if (value === null || value === undefined) return '-'; - return typeof value === 'number' ? value.toString() : value; - }; - // Ensure type is set to number to prevent date formatting - column.type = 'number'; - } - - return column; - }); -}; - -// Hook factory function for workflows data -const createWorkflowsHook = () => { - return () => { - const { - data: workflows, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - attributes, - permissions, - pagination, - fetchWorkflowById, - generateEditFieldsFromAttributes, - ensureAttributesLoaded - } = useUserWorkflows(); - const { - handleWorkflowDelete, - handleWorkflowDeleteMultiple, - handleWorkflowUpdate, - deletingWorkflows, - editingWorkflows - } = useWorkflowOperations(); - - const generatedColumns = attributes && attributes.length > 0 - ? attributesToColumns(attributes) - : undefined; - - // Handle single workflow deletion for FormGenerator - const handleDeleteSingle = useCallback(async (workflow: any) => { - const success = await handleWorkflowDelete(workflow.id); - - if (success) { - refetch(); - } - }, [handleWorkflowDelete, refetch]); - - // Handle multiple workflow deletion for FormGenerator - const handleDeleteMultiple = useCallback(async (selectedWorkflows: any[]) => { - const workflowIds = selectedWorkflows.map(workflow => workflow.id); - const success = await handleWorkflowDeleteMultiple(workflowIds); - - if (success) { - refetch(); - } - }, [handleWorkflowDeleteMultiple, refetch]); - - return { - data: workflows, - loading, - error, - refetch, - removeOptimistically, - updateOptimistically, - // Operations - handleDelete: handleWorkflowDelete, - handleDeleteMultiple, - handleWorkflowUpdate, - // FormGenerator specific handlers - onDelete: handleDeleteSingle, - onDeleteMultiple: handleDeleteMultiple, - // Loading states - deletingWorkflows, - editingWorkflows, - // Attributes and permissions for dynamic column/button generation - attributes, - permissions, - pagination, // Pagination metadata from backend - columns: generatedColumns, // Return generated columns - // Functions for EditActionButton - fetchWorkflowById, // Fetch single workflow by ID - generateEditFieldsFromAttributes, // Generate edit fields from attributes - ensureAttributesLoaded // Generic function to ensure attributes are loaded - }; - }; -}; - -export const workflowsPageData: GenericPageData = { - id: 'workflows-list', - path: 'workflows/list', - name: 'workflows.title', - description: 'workflows.description', - - // Parent page - now under 'workflows' group - parentPath: 'workflows', - - // Visual - icon: FaProjectDiagram, - title: 'workflows.title', - subtitle: 'workflows.title', - - // Header buttons - none for now, workflows are typically created through the playground - - // Content sections - using generic table approach - content: [ - { - id: 'workflows-table', - type: 'table', - tableConfig: { - hookFactory: createWorkflowsHook, - // Columns are generated dynamically from attributes via hookData.columns - actionButtons: [ - { - type: 'play', - title: 'workflows.action.play', - idField: 'id', - nameField: 'name', - navigateTo: 'start/dashboard', - mode: 'workflow', // Set mode to 'workflow' to select workflow instead of setting prompt - // Only show if user has read permission (permissions.read !== 'n') - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasRead = hookData.permissions.read !== 'n' && hookData.permissions.view; - return { disabled: !hasRead, message: 'No permission to start workflows' }; - } - }, - { - type: 'edit', - title: 'workflows.action.edit', - idField: 'id', - nameField: 'name', - operationName: 'handleWorkflowUpdate', - loadingStateName: 'editingWorkflows', - fetchItemFunctionName: 'fetchWorkflowById', - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view; - return { disabled: !hasUpdate, message: 'No permission to edit workflows' }; - } - }, - { - type: 'delete', - title: 'workflows.action.delete', - idField: 'id', - operationName: 'handleDelete', - loadingStateName: 'deletingWorkflows', - // Only show if user has delete permission (permissions.delete !== 'n') - disabled: (hookData: any) => { - if (!hookData?.permissions) return { disabled: false }; - const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view; - return { disabled: !hasDelete, message: 'No permission to delete workflows' }; - } - } - ], - searchable: true, - filterable: true, - sortable: true, - resizable: true, - pagination: true, - pageSize: 10, - className: 'workflows-table' - } - } - ], - - // Page behavior - persistent: false, - preload: false, - preserveState: true, // Keep page mounted and prevent refetching - moduleEnabled: true, - - // Sidebar - will be shown as subpage under Administration - - // Lifecycle hooks - onActivate: async () => { - if (import.meta.env.DEV) console.log('Workflows activated'); - }, - onLoad: async () => { - if (import.meta.env.DEV) console.log('Workflows loaded - can initialize workflow lists here'); - }, - onUnload: async () => { - if (import.meta.env.DEV) console.log('Workflows unloaded - cleanup workflow references'); - } -}; diff --git a/src/core/PageManager/index.ts b/src/core/PageManager/index.ts deleted file mode 100644 index c0dbbc5..0000000 --- a/src/core/PageManager/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @deprecated This PageManager system is deprecated. - * - * New pages should be created in src/pages/ and use: - * - src/components/Navigation/MandateNavigation.tsx for navigation - * - src/App.tsx for routing - * - * Migration targets (new location): - * - workflows → /workflows/list - * - automations → /workflows/automations - * - playground → /workflows/playground - * - prompts → /basedata/prompts - * - files → /basedata/files - * - connections → /basedata/connections - * - chatbot → /chatbot (migrate to feature) - * - pek → /pek (migrate to feature) - * - speech → /speech (migrate to feature) - * - * This module is kept for backward compatibility with Sidebar.tsx - * and will be fully removed in a future release. - */ - -// Export the page management system (DEPRECATED) -export { default as PageManager } from './PageManager'; -export { default as PageRenderer } from './PageRenderer'; -export { default as SidebarProvider } from './SidebarProvider'; - -// Export data and interfaces (DEPRECATED) -export * from './data'; -export * from './pageInterface'; \ No newline at end of file diff --git a/src/core/PageManager/pageInterface.ts b/src/core/PageManager/pageInterface.ts deleted file mode 100644 index 5c35ddf..0000000 --- a/src/core/PageManager/pageInterface.ts +++ /dev/null @@ -1,429 +0,0 @@ -import React from 'react'; -import { IconType } from 'react-icons'; -import { DragDropConfig } from '../../components/UiComponents/DragDropOverlay/DragDropOverlay'; - -// Generic privilege checker function type -export type PrivilegeChecker = () => boolean | Promise; - -// Form field configuration for create/edit buttons -export interface ButtonFormField { - key: string; - label: string | LanguageText; - type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'readonly' | 'multiselect' | 'number'; - required?: boolean; - placeholder?: string | LanguageText; - minRows?: number; - maxRows?: number; - validator?: (value: any) => string | null; - defaultValue?: any; - options?: string[] | Array<{ value: string | number; label: string }>; // For enum/multiselect fields - optionsReference?: string; // Reference to a data source for dynamic options (e.g., 'TrusteeOrganisation', 'User') -} - -// Dropdown configuration for header dropdown buttons -export interface DropdownConfig { - type: 'dropdown'; - items: Array<{ - id: string | number; - label: string | LanguageText; - value: T; - metadata?: Record; - }>; - selectedItemId?: string | number | null; - onSelect: (item: { id: string | number; label: string | LanguageText; value: T; metadata?: Record } | null, hookData?: any) => void | Promise; - placeholder?: string | LanguageText; - emptyMessage?: string | LanguageText; - headerText?: string | LanguageText; - // Optional: name of property in hookData that provides items, selectedItemId, and onSelect - dataSource?: { - itemsProperty?: string; // Property name in hookData that contains items array - selectedIdProperty?: string; // Property name in hookData that contains selectedItemId - onSelectMethod?: string; // Method name in hookData for onSelect callback - loadingProperty?: string; // Property name in hookData that contains loading state - }; -} - -// Button configuration for header actions -export interface PageButton { - id: string; - label: string | LanguageText; - variant?: 'primary' | 'secondary' | 'danger' | 'success' | 'warning'; - size?: 'sm' | 'md' | 'lg'; - icon?: IconType; - onClick?: (hookData?: any) => void | Promise; - disabled?: boolean | ((hookData?: any) => boolean | { disabled: boolean; message?: string }); - // Form configuration for create buttons - formConfig?: { - fields: ButtonFormField[]; - popupTitle?: string | LanguageText; - popupSize?: 'small' | 'medium' | 'large'; - createOperationName?: string; // Name of the create operation in hookData (e.g., 'handlePromptCreate') - successMessage?: string | LanguageText; - errorMessage?: string | LanguageText; - multiStep?: boolean; // Enable multi-step form mode - }; - // Dropdown configuration for dropdown selection buttons - dropdownConfig?: DropdownConfig; -} - -// Input form configuration -export interface InputFormConfig { - hookFactory?: () => () => GenericDataHook; // Optional, can use page-level hook from tableConfig if exists - placeholder?: string | LanguageText; - buttonLabel?: string | LanguageText; - stopButtonLabel?: string | LanguageText; // Label for stop button when workflow is running - buttonIcon?: IconType; - stopButtonIcon?: IconType; // Icon for stop button when workflow is running - buttonVariant?: 'primary' | 'secondary' | 'danger' | 'success' | 'warning'; - stopButtonVariant?: 'primary' | 'secondary' | 'danger' | 'success' | 'warning'; // Variant for stop button - buttonSize?: 'sm' | 'md' | 'lg'; - textFieldSize?: 'sm' | 'md' | 'lg'; - showFileUpload?: boolean; // Whether to show file upload button (default: true if hook provides file upload) -} - -// Settings field configuration -export interface SettingsFieldConfig { - id: string; - type: 'text' | 'select' | 'toggle'; - label: string | LanguageText; - description?: string | LanguageText; - required?: boolean; - disabled?: boolean | ((data: any) => boolean); - inputType?: 'text' | 'email' | 'tel'; - placeholder?: string | LanguageText; - options?: Array<{id: string | number; label: string | LanguageText; value: any}>; - dataKey: string; // Dot-notation path (e.g., "mandate_general.company_name") - onSave?: (value: any, data: any) => Promise | void; -} - -// Settings section configuration -export interface SettingsSectionConfig { - id: string; - title: string | LanguageText; - description?: string | LanguageText; - icon?: IconType; // Optional icon for section header - // Fields will be loaded from backend based on sectionId - sectionId: string; // Identifier sent to backend to fetch fields - // Save handler per section (uses Button component) - onSave?: (sectionId: string, data: any) => Promise; - saveButtonLabel?: string | LanguageText; - saveButtonVariant?: 'primary' | 'secondary' | 'danger' | 'success' | 'warning'; - saveButtonSize?: 'sm' | 'md' | 'lg'; - // Optional: Static field overrides from config (takes precedence over backend) - staticFields?: SettingsFieldConfig[]; - // Optional: Conditional rendering - if provided, section only renders if condition returns true - renderCondition?: (formData: any) => boolean; - // Optional: Alternative content to render when renderCondition returns false - renderAlternative?: (formData: any, t: (key: string) => string, resolveLanguageText: (text: string | LanguageText, t: (key: string) => string) => string) => React.ReactNode; -} - -// Settings configuration -export interface SettingsConfig { - sections: SettingsSectionConfig[]; - hookFactory?: () => () => GenericDataHook; // For data fetching and field loading -} - -// Content section for paragraphs -export interface PageContent { - id: string; - type: 'paragraph' | 'heading' | 'list' | 'code' | 'divider' | 'custom' | 'table' | 'inputForm' | 'messages' | 'settings' | 'log' | 'tabs' | 'columns' | 'chatHistory'; - content?: string | LanguageText; // Optional for dividers - level?: number; // For headings (1-6) - items?: (string | LanguageText)[]; // For lists - language?: string; // For code blocks - customComponent?: React.ComponentType; - // Table-specific properties - tableConfig?: TableContentConfig; - // Input form-specific properties - inputFormConfig?: InputFormConfig; - // Messages-specific properties - messagesConfig?: { - variant?: 'chat' | 'log'; // Message display variant: 'chat' for bubble UI, 'log' for list/table UI - dataSource?: 'messages' | 'logs' | 'both'; // Data source to render: 'messages' (default), 'logs', or 'both' (logs use log variant when merged) - showDocuments?: boolean; - showMetadata?: boolean; - showProgress?: boolean; - emptyMessage?: string | LanguageText; - }; - // Settings-specific properties - settingsConfig?: SettingsConfig; - // Log-specific properties - logConfig?: { - emptyMessage?: string | LanguageText; - }; - // Chat history-specific properties - chatHistoryConfig?: { - emptyMessage?: string | LanguageText; - }; - // Tabs-specific properties - tabsConfig?: { - tabs: Array<{ - id: string; - label: string | LanguageText; - content: PageContent[]; // Nested content sections for each tab - }>; - defaultTabId?: string; - }; - // Columns-specific properties - columnsConfig?: { - columns: Array<{ - id: string; - width?: string; // CSS width (e.g., "3fr", "1fr", "75%", "25%") - content: PageContent[]; // Nested content sections for each column - }>; - gap?: string; // CSS gap value - }; -} - -// Generic hook interface for data fetching -export interface GenericDataHook { - data: any[]; - loading: boolean; - isRefetching?: boolean; // True when refetching data (keeps existing data visible) - error: string | null; - refetch?: (params?: { page?: number; pageSize?: number; sort?: Array<{field: string; direction: 'asc' | 'desc'}>; filters?: any; search?: string }) => Promise; - pagination?: { - currentPage: number; - pageSize: number; - totalItems: number; - totalPages: number; - sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; - filters?: any; - } | null; - removeFileOptimistically?: (fileId: string) => void; // For optimistic updates - columns?: any[]; // Optional columns configuration - // File operations - handleUpload?: (file: File) => Promise<{ success: boolean; data: any }>; // For file upload functionality - handleFileUpload?: (file: File) => Promise<{ success: boolean; data: any }>; // Alias for handleUpload - handleDownload?: (fileId: string, fileName: string) => Promise; // For file download functionality - handleDelete?: (fileId: string, onOptimisticDelete?: () => void) => Promise; // For file delete functionality - handleFileDelete?: ((fileId: string, onOptimisticDelete?: () => void) => Promise) | ((file: any) => Promise); // Can accept fileId or WorkflowFile - handlePreview?: (fileId: string, fileName: string, mimeType?: string) => Promise; // For file preview functionality - // File management properties - workflowFiles?: any[]; // Files connected to workflow - pendingFiles?: any[]; // Files pending attachment - allUserFiles?: any[]; // All user files - handleFileRemove?: ((fileId: string) => Promise | void) | ((file: any) => Promise | void); // Can accept fileId or WorkflowFile - handleFileAttach?: (fileId: string) => Promise; // Attach file to workflow (always returns Promise) - handleFileUploadAndAttach?: (file: File) => Promise<{ success: boolean; data: any }>; // Upload and attach file - uploadingFile?: boolean; // Loading state for file upload - deletingFiles?: Set; // Set of file IDs being deleted - previewingFiles?: Set; // Set of file IDs being previewed - removingFiles?: Set; // Set of file IDs being removed - isFileAttachmentPopupOpen?: boolean; // Whether file attachment popup is open - setIsFileAttachmentPopupOpen?: (open: boolean) => void; // Set file attachment popup state - // FormGenerator specific handlers - onDelete?: (row: any) => Promise; // For single item deletion - onDeleteMultiple?: (rows: any[]) => Promise; // For multiple item deletion - // Input form operations - inputValue?: string; - onInputChange?: (value: string) => void; - handleSubmit?: () => Promise; // No parameters, uses internal inputValue - isSubmitting?: boolean; - // Prompt selector properties - promptPermission?: { - view?: boolean; - read?: string; - }; - promptItems?: Array<{ id: string | number; label: string; value: any }>; - selectedPromptId?: string | number | null; - onPromptSelect?: (item: { id: string | number; label: string; value: any } | null) => void | Promise; - promptsLoading?: boolean; - // Workflow mode selector properties - workflowModeItems?: Array<{ id: string | number; label: string; value: any }>; - selectedWorkflowMode?: string | number | null; - onWorkflowModeSelect?: (item: { id: string | number; label: string; value: any } | null) => void | Promise; - // Workflow lifecycle state - workflowId?: string; - workflowStatus?: string; - workflowData?: { - currentRound?: number; - [key: string]: any; - }; - isRunning?: boolean; - currentRound?: number; // Current workflow round - latestStats?: any; // Latest workflow statistics - // Messages from workflow - messages?: any[]; - // Logs from workflow - logs?: any[]; - // Dashboard log tree - dashboardTree?: any; // Dashboard log tree structure - onToggleOperationExpanded?: (operationId: string) => void; - getChildOperations?: (parentId: string | null) => string[]; - // Settings-specific properties - settingsData?: any; // Unified data object for settings fields - settingsFields?: Record; // Field definitions per sectionId - settingsLoading?: Record; // Loading state per section - settingsErrors?: Record; // Error state per section - saveSection?: (sectionId: string, data: any) => Promise; // Save handler for a section - // Dropdown data source loading property - [key: string]: any; // Allow additional properties for dynamic data sources -} - -// Standard action button configuration (built-in actions: edit, delete, view, copy, connect, play) -export interface ActionButtonConfig { - type: 'view' | 'edit' | 'delete' | 'copy' | 'connect' | 'play'; - onAction?: (row: any) => Promise | void; // Optional for delete buttons since they handle their own logic - title?: string | LanguageText; - disabled?: (row: any, hookData?: any) => boolean | { disabled: boolean; message?: string }; - loading?: (row: any, hookData?: any) => boolean; - // Field mappings for flexible data access - idField?: string; // Field name for the unique identifier (default: 'id') - nameField?: string; // Field name for display name (default: 'name' or 'file_name') - typeField?: string; // Field name for type/mime type (default: 'type' or 'mime_type') - contentField?: string; // Field name for content (default: 'content') - statusField?: string; // Field name for status (used by connect action) - // Operation and loading state names - operationName?: string; // Name of the operation function in hookData - loadingStateName?: string; // Name of the loading state in hookData - fetchItemFunctionName?: string; // Name of the function in hookData to fetch a single item by ID (for edit button) - // Navigation and mode (for play action) - navigateTo?: string; // Path to navigate to after action - mode?: string; // Mode to set (e.g., 'prompt', 'workflow') -} - -// Custom action button configuration (for entity-specific actions like download, connect, play, sendPasswordLink) -export interface CustomActionConfig { - id: string; // Unique identifier for the action - icon: React.ReactNode; // Icon component to display - onClick: (row: any, hookData?: any) => Promise | void; // Handler function - visible?: (row: any, hookData?: any) => boolean; // Show/hide based on row data (default: true) - disabled?: (row: any, hookData?: any) => boolean | { disabled: boolean; message?: string }; // Disable based on row data - loading?: (row: any, hookData?: any) => boolean; // Loading state based on row data - title?: string | LanguageText | ((row: any) => string); // Tooltip text - className?: string; // Optional custom CSS class - // Field mappings (optional, for convenience) - idField?: string; // Field name for the unique identifier (default: 'id') -} - -// Table content configuration -export interface TableContentConfig { - hookFactory: () => () => GenericDataHook; // Hook factory that returns a hook function - columns?: any[]; // Column configuration (optional - can be generated dynamically from attributes via hookData.columns) - actionButtons?: ActionButtonConfig[]; // Standard action buttons configuration (edit, delete, view, copy) - customActions?: CustomActionConfig[]; // Custom action buttons (download, connect, play, sendPasswordLink, etc.) - searchable?: boolean; - filterable?: boolean; - sortable?: boolean; - resizable?: boolean; - pagination?: boolean; - pageSize?: number; - className?: string; - emptyMessage?: string; // Custom message to display when table is empty -} - -// Language-aware text interface -export interface LanguageText { - de: string; - en: string; - fr: string; -} - -// Utility function to resolve language text -export const resolveLanguageText = (text: string | LanguageText | undefined, t?: (key: string, fallback?: string) => string): string => { - if (!text) return ''; - if (typeof text === 'string') { - // Always use the translation function for strings (language keys) - if (t) { - return t(text); - } - return text; - } - // For LanguageText objects, we should convert them to language keys - // For now, fallback to the first available language - return text.de || text.en || text.fr || ''; -}; - -// Generic page data interface -export interface GenericPageData { - // Core identification - id: string; - path: string; - name: string; - description?: string | LanguageText; - - // Navigation - parentPath?: string; // For subpages/subsubpages - order?: number; - showInSidebar?: boolean; - - // Visual - icon?: IconType; - title: string | LanguageText; - subtitle?: string | LanguageText; - - // Header configuration - headerButtons?: PageButton[]; - - // Content sections - content?: PageContent[]; - - // Page behavior - persistent?: boolean; - preserveState?: boolean; - preload?: boolean; - moduleEnabled?: boolean; - hide?: boolean; // If true, page is completely hidden and not rendered - - // Subpage support - hasSubpages?: boolean; - - // Lifecycle hooks - onActivate?: () => void | Promise; - onDeactivate?: () => void | Promise; - onLoad?: () => void | Promise; - onUnload?: () => void | Promise; - - // Custom component override (optional) - customComponent?: React.ComponentType; - - // Privilege checker - if provided, page will only render if checker returns true - privilegeChecker?: PrivilegeChecker; - - // Drag and drop configuration - dragDropConfig?: DragDropConfig; -} - -// Page data file structure -export interface PageDataFile { - page: GenericPageData; - subpages?: PageDataFile[]; -} - -// Sidebar item interface for compatibility -export interface SidebarItem { - id: string; - name: string; - link: string | undefined; // Allow undefined for parent groups that aren't clickable pages - icon?: IconType | React.ComponentType>; // Allow both IconType and SVG components - moduleEnabled: boolean; - order: number; - submenu?: SidebarSubmenuItemData[]; - depth?: number; // Hierarchy depth for indentation (0 = top level) -} - -// Sidebar submenu item data interface -export interface SidebarSubmenuItemData { - id: string; - name: string; - link?: string; // Optional - if undefined, it's a navigation node (not a page) - icon?: IconType | React.ComponentType>; // Allow both IconType and SVG components - submenu?: SidebarSubmenuItemData[]; // Recursive support for nested submenus - depth?: number; // Hierarchy depth for indentation (0 = top level) -} - -// Page instance for PageManager -export interface PageInstance { - path: string; - component: React.ReactElement; - isActive: boolean; - shouldPreserve: boolean; - pageData: GenericPageData; -} - -// Page manager props -export interface PageManagerProps { - loadingComponent: React.ComponentType; - errorComponent: React.ComponentType; -} diff --git a/src/hooks/useAccessRules.ts b/src/hooks/useAccessRules.ts deleted file mode 100644 index 2a56499..0000000 --- a/src/hooks/useAccessRules.ts +++ /dev/null @@ -1,243 +0,0 @@ -/** - * useAccessRules Hook - * - * Hook for managing RBAC access rules for a role. - * Supports both system admin (template roles) and feature admin (instance roles). - */ - -import { useState, useCallback } from 'react'; -import api from '../api'; - -// ============================================================================= -// TYPES -// ============================================================================= - -export type RuleContext = 'DATA' | 'UI' | 'RESOURCE'; -export type AccessLevel = 'n' | 'm' | 'g' | 'a' | null; - -// ============================================================================= -// ACCESS LEVEL LABELS -// ============================================================================= - -export const ACCESS_LEVEL_OPTIONS: { value: 'n' | 'm' | 'g' | 'a'; label: string; color: string }[] = [ - { value: 'n', label: 'Keine', color: '#e53e3e' }, - { value: 'm', label: 'Eigene', color: '#d69e2e' }, - { value: 'g', label: 'Gruppe', color: '#3182ce' }, - { value: 'a', label: 'Alle', color: '#38a169' }, -]; - -export const getAccessLevelLabel = (level: AccessLevel | null): string => { - if (!level) return '-'; - const option = ACCESS_LEVEL_OPTIONS.find(opt => opt.value === level); - return option?.label || level; -}; - -export const getAccessLevelColor = (level: AccessLevel | null): string => { - if (!level) return '#718096'; - const option = ACCESS_LEVEL_OPTIONS.find(opt => opt.value === level); - return option?.color || '#718096'; -}; - -export interface AccessRule { - id: string; - roleId: string; - context: RuleContext; - item: string | null; - view: boolean; - read: AccessLevel; - create: AccessLevel; - update: AccessLevel; - delete: AccessLevel; -} - -export interface AccessRuleCreate { - context: RuleContext; - item?: string | null; - view?: boolean; - read?: AccessLevel; - create?: AccessLevel; - update?: AccessLevel; - delete?: AccessLevel; -} - -interface GroupedRules { - DATA: AccessRule[]; - UI: AccessRule[]; - RESOURCE: AccessRule[]; -} - -interface SaveResult { - success: boolean; - error?: string; -} - -// ============================================================================= -// HOOK -// ============================================================================= - -export function useAccessRules(roleId: string, apiBasePath: string = '/api/rbac') { - const [rules, setRules] = useState([]); - const [loading, setLoading] = useState(false); - const [saving, setSaving] = useState(false); - const [error, setError] = useState(null); - - // Determine if this is a feature-instance API path - const isInstanceApi = apiBasePath.includes('/instance-roles/'); - - /** - * Fetch all rules for the role - */ - const fetchRules = useCallback(async (): Promise => { - setLoading(true); - setError(null); - - try { - // Different endpoint structure for instance roles vs system roles - const endpoint = isInstanceApi - ? `${apiBasePath}/rules` - : `${apiBasePath}/rules/by-role/${roleId}`; - - const response = await api.get(endpoint); - const fetchedRules = response.data?.items || response.data || []; - setRules(fetchedRules); - return fetchedRules; - } catch (err: any) { - const errorMsg = err.response?.data?.detail || err.message || 'Fehler beim Laden der Regeln'; - setError(errorMsg); - console.error('Error fetching rules:', err); - return []; - } finally { - setLoading(false); - } - }, [roleId, apiBasePath, isInstanceApi]); - - /** - * Save all rules for the role - */ - const saveRules = useCallback(async (rulesToSave: AccessRule[]): Promise => { - setSaving(true); - setError(null); - - try { - // Different endpoint structure for instance roles vs system roles - const rulesEndpoint = isInstanceApi - ? `${apiBasePath}/rules` - : `${apiBasePath}/rules/by-role/${roleId}`; - - // Get current rules from server - const currentResponse = await api.get(rulesEndpoint); - const currentRules: AccessRule[] = currentResponse.data?.items || currentResponse.data || []; - const currentRuleIds = new Set(currentRules.map(r => r.id)); - - // Determine changes - const newRules = rulesToSave.filter(r => r.id.startsWith('temp-')); - const existingRules = rulesToSave.filter(r => !r.id.startsWith('temp-')); - const deletedRuleIds = [...currentRuleIds].filter( - id => !existingRules.some(r => r.id === id) - ); - - // Delete removed rules - for (const deletedId of deletedRuleIds) { - const deleteEndpoint = isInstanceApi - ? `${apiBasePath}/rules/${deletedId}` - : `${apiBasePath}/rules/${deletedId}`; - await api.delete(deleteEndpoint); - } - - // Create new rules - for (const rule of newRules) { - const createEndpoint = isInstanceApi - ? `${apiBasePath}/rules` - : `${apiBasePath}/rules`; - await api.post(createEndpoint, { - roleId, - context: rule.context, - item: rule.item, - view: rule.view, - read: rule.read, - create: rule.create, - update: rule.update, - delete: rule.delete, - }); - } - - // Update existing rules - for (const rule of existingRules) { - const original = currentRules.find(r => r.id === rule.id); - if (original && JSON.stringify(original) !== JSON.stringify(rule)) { - const updateEndpoint = isInstanceApi - ? `${apiBasePath}/rules/${rule.id}` - : `${apiBasePath}/rules/${rule.id}`; - await api.put(updateEndpoint, { - view: rule.view, - read: rule.read, - create: rule.create, - update: rule.update, - delete: rule.delete, - }); - } - } - - // Refresh rules - await fetchRules(); - - return { success: true }; - } catch (err: any) { - const errorMsg = err.response?.data?.detail || err.message || 'Fehler beim Speichern'; - setError(errorMsg); - console.error('Error saving rules:', err); - return { success: false, error: errorMsg }; - } finally { - setSaving(false); - } - }, [roleId, apiBasePath, isInstanceApi, fetchRules]); - - /** - * Get rules grouped by context - */ - const getGroupedRules = useCallback((): GroupedRules => { - return { - DATA: rules.filter(r => r.context === 'DATA'), - UI: rules.filter(r => r.context === 'UI'), - RESOURCE: rules.filter(r => r.context === 'RESOURCE'), - }; - }, [rules]); - - /** - * Update a rule locally (not saved until saveRules is called) - */ - const updateRuleLocally = useCallback((ruleId: string, updates: Partial) => { - setRules(prev => prev.map(r => - r.id === ruleId ? { ...r, ...updates } : r - )); - }, []); - - /** - * Add a rule locally (not saved until saveRules is called) - */ - const addRuleLocally = useCallback((rule: AccessRule) => { - setRules(prev => [...prev, rule]); - }, []); - - /** - * Remove a rule locally (not saved until saveRules is called) - */ - const removeRuleLocally = useCallback((ruleId: string) => { - setRules(prev => prev.filter(r => r.id !== ruleId)); - }, []); - - return { - rules, - loading, - saving, - error, - fetchRules, - saveRules, - getGroupedRules, - updateRuleLocally, - addRuleLocally, - removeRuleLocally, - }; -} - -export default useAccessRules; diff --git a/src/hooks/useChatbot.ts b/src/hooks/useChatbot.ts deleted file mode 100644 index da39ba6..0000000 --- a/src/hooks/useChatbot.ts +++ /dev/null @@ -1,771 +0,0 @@ -import { useState, useCallback, useRef, useMemo, useEffect } from 'react'; -import { useApiRequest } from './useApi'; -import api from '../api'; -import { - startChatbotStreamApi, - stopChatbotApi, - getChatbotThreadsApi, - getChatbotThreadApi, - deleteChatbotWorkflowApi, - type ChatDataItem, - type StartChatbotRequest, - type ChatbotWorkflow -} from '../api/chatbotApi'; -import { Message } from '../components/UiComponents/Messages/MessagesTypes'; -// Simple sort function for messages -const sortMessages = (a: Message, b: Message) => { - if (a.publishedAt !== undefined && b.publishedAt !== undefined) { - return a.publishedAt - b.publishedAt; - } - if (a.publishedAt !== undefined) return -1; - if (b.publishedAt !== undefined) return 1; - if (a.sequenceNr !== undefined && b.sequenceNr !== undefined) { - return a.sequenceNr - b.sequenceNr; - } - return 0; -}; - -export function useChatbot() { - const [inputValue, setInputValue] = useState(''); - const [messages, setMessages] = useState([]); - const [workflowId, setWorkflowId] = useState(null); - const [isRunning, setIsRunning] = useState(false); - const [isSubmitting, setIsSubmitting] = useState(false); - const [error, setError] = useState(null); - - // File upload state - const [pendingFileIds, setPendingFileIds] = useState([]); - const pendingFileIdsRef = useRef([]); // Ref to avoid closure issues - const [uploadingFile, setUploadingFile] = useState(false); - const [uploadError, setUploadError] = useState(null); - const [uploadedFiles, setUploadedFiles] = useState>([]); - - // Chat history state - const [threads, setThreads] = useState([]); - const [selectedThreadId, setSelectedThreadId] = useState(null); - const [threadsLoading, setThreadsLoading] = useState(false); - const [threadsError, setThreadsError] = useState(null); - const [deletingThreads, setDeletingThreads] = useState>(new Set()); - - const { request } = useApiRequest(); - const streamAbortControllerRef = useRef(null); - const processedMessageIdsRef = useRef>(new Set()); - const thinkingMessageIdRef = useRef(null); - const thinkingLogsRef = useRef([]); // Use ref instead of state to avoid batching - const logQueueRef = useRef([]); // Queue for logs to process one by one - const isProcessingLogsRef = useRef(false); // Flag to prevent concurrent processing - const processedLogsRef = useRef>(new Set()); // Track processed logs to prevent duplicates - - // Clear processed message IDs when workflow changes - const clearProcessedMessages = useCallback(() => { - processedMessageIdsRef.current.clear(); - }, []); - - // Clear thinking message when a new assistant message arrives - const clearThinkingMessage = useCallback(() => { - // Clear log queue and stop processing - logQueueRef.current = []; - isProcessingLogsRef.current = false; - processedLogsRef.current.clear(); // Clear processed logs tracking - - // Reset thinking message refs - thinkingMessageIdRef.current = null; - thinkingLogsRef.current = []; - - // Remove ALL thinking messages (not just the one with current ID) - // This handles cases where multiple thinking messages might exist - setMessages(prevMessages => { - return prevMessages.filter(m => m.status !== 'thinking'); - }); - }, []); - - // Process logs from queue one by one (progressive display) - const processLogQueue = useCallback(() => { - if (isProcessingLogsRef.current || logQueueRef.current.length === 0) { - return; - } - - isProcessingLogsRef.current = true; - const logMessage = logQueueRef.current.shift()!; - - // Add log to accumulated logs - thinkingLogsRef.current = [...thinkingLogsRef.current, logMessage]; - - // Get or create thinking message ID - const thinkingId = thinkingMessageIdRef.current || `thinking-${Date.now()}`; - thinkingMessageIdRef.current = thinkingId; - - // Create/update thinking message with all accumulated logs - // Format logs as Markdown list items so each log appears on a separate line - const formattedLogs = thinkingLogsRef.current - .map(log => `- ${log.trim()}`) - .join('\n'); - - const thinkingMessage: Message = { - id: thinkingId, - workflowId: workflowId || '', - role: 'assistant', - message: formattedLogs, - publishedAt: Date.now() - 1, - status: 'thinking' - }; - - // Update messages immediately - setMessages(prevMessages => { - // Remove ALL thinking messages first (to prevent duplicates from previous workflows) - const filtered = prevMessages.filter(m => m.status !== 'thinking'); - // Add updated thinking message - const updated = [...filtered, thinkingMessage]; - return updated.sort(sortMessages); - }); - - // Process next log after a small delay (progressive display) - if (logQueueRef.current.length > 0) { - setTimeout(() => { - isProcessingLogsRef.current = false; - processLogQueue(); - }, 50); // Small delay between logs for progressive display - } else { - isProcessingLogsRef.current = false; - } - }, [workflowId]); - - // Add a single log to thinking message (progressive display) - const addLogToThinkingMessage = useCallback((logMessage: string, createdAt?: number) => { - // Create a unique key for this log message to detect duplicates - // Use content + createdAt timestamp if available, otherwise use current time - const timestamp = createdAt || Date.now(); - const logKey = `${logMessage.trim()}_${timestamp}`; - - // Skip if this log was already processed - if (processedLogsRef.current.has(logKey)) { - console.log('[useChatbot] Skipping duplicate log:', logMessage.substring(0, 50) + '...'); - return; - } - - // Mark as processed - processedLogsRef.current.add(logKey); - - // Add log to queue - logQueueRef.current.push(logMessage); - - // Start processing if not already processing - if (!isProcessingLogsRef.current) { - processLogQueue(); - } - }, [processLogQueue]); - - // Process SSE event and update messages - const processChatDataItem = useCallback((item: ChatDataItem) => { - // Log the actual streamed response for debugging - console.log('[useChatbot] Streamed item:', { - type: item.type, - createdAt: item.createdAt, - item: item.item - }); - - if (item.type === 'log' && item.item) { - // Process log item - add to thinking message one at a time - const logData = item.item as any; - const logMessage = logData.message || logData.text || ''; - - console.log('[useChatbot] Processing log:', { - message: logMessage.substring(0, 100) + (logMessage.length > 100 ? '...' : ''), - createdAt: item.createdAt, - fullItem: item - }); - - if (logMessage) { - // Add log immediately (progressive display) with createdAt for deduplication - addLogToThinkingMessage(logMessage, item.createdAt); - } - } else if (item.type === 'message' && item.item) { - const messageData = item.item as any; - - // Ensure message has required fields - if (!messageData.id) { - console.warn('⚠️ Invalid message item (missing id):', messageData); - return; - } - - // ALWAYS clear thinking message when ANY message arrives (not just assistant) - // The thinking message should disappear when the real message comes - - // Check if we've already processed this message - const messageId = messageData.id; - - // Always clear thinking messages when a real message arrives - clearThinkingMessage(); - - if (processedMessageIdsRef.current.has(messageId)) { - // Update existing message - setMessages(prevMessages => { - const existingIndex = prevMessages.findIndex(m => m.id === messageId); - if (existingIndex >= 0) { - const updated = [...prevMessages]; - updated[existingIndex] = messageData as Message; - return updated.sort(sortMessages); - } - return prevMessages; - }); - } else { - // Add new message - processedMessageIdsRef.current.add(messageId); - setMessages(prevMessages => { - // Add new message (thinking messages already cleared by clearThinkingMessage) - const updated = [...prevMessages, messageData as Message]; - return updated.sort(sortMessages); - }); - } - } - }, [addLogToThinkingMessage]); - - // Load all threads (with loading state) - const loadThreads = useCallback(async () => { - try { - setThreadsLoading(true); - setThreadsError(null); - - const result = await getChatbotThreadsApi(request); - - // Sort threads by lastActivity (newest first) - const sortedThreads = [...result.items].sort((a, b) => { - const aTime = a.lastActivity || a.startedAt || 0; - const bTime = b.lastActivity || b.startedAt || 0; - return bTime - aTime; // Descending order (newest first) - }); - - setThreads(sortedThreads); - } catch (err: any) { - console.error('Error loading threads:', err); - setThreadsError(err.message || 'Failed to load threads'); - } finally { - setThreadsLoading(false); - } - }, [request]); - - // Load threads silently (without loading state) - keeps existing threads visible - const loadThreadsSilently = useCallback(async () => { - try { - setThreadsError(null); - - const result = await getChatbotThreadsApi(request); - - // Sort threads by lastActivity (newest first) - const sortedThreads = [...result.items].sort((a, b) => { - const aTime = a.lastActivity || a.startedAt || 0; - const bTime = b.lastActivity || b.startedAt || 0; - return bTime - aTime; // Descending order (newest first) - }); - - setThreads(sortedThreads); - } catch (err: any) { - console.error('Error loading threads silently:', err); - setThreadsError(err.message || 'Failed to load threads'); - } - }, [request]); - - // Load a specific workflow and its messages - const loadWorkflow = useCallback(async (workflowIdToLoad: string) => { - try { - setError(null); - - const result = await getChatbotThreadApi(request, workflowIdToLoad); - - console.log('[loadWorkflow] Full result:', JSON.stringify(result, null, 2)); - console.log('[loadWorkflow] chatData structure:', { - isArray: Array.isArray(result.chatData), - isObject: result.chatData && typeof result.chatData === 'object', - chatDataKeys: result.chatData && typeof result.chatData === 'object' ? Object.keys(result.chatData) : [], - hasItems: result.chatData?.items !== undefined, - itemsLength: Array.isArray(result.chatData?.items) ? result.chatData.items.length : 'not an array' - }); - - // Convert chatData to Message[] - const loadedMessages: Message[] = []; - const processedIds = new Set(); - - // Backend returns chatData as { items: ChatDataItem[] } structure - const chatDataArray: any[] = result.chatData?.items || []; - - console.log('[loadWorkflow] Processing chatDataArray:', { - length: chatDataArray.length, - types: chatDataArray.map((item, idx) => ({ - index: idx, - type: item?.type, - hasItem: !!item?.item, - keys: item ? Object.keys(item) : [] - })) - }); - - for (const item of chatDataArray) { - // Handle ChatDataItem structure: { type: 'message', createdAt: number, item: Message } - if (item.type === 'message' && item.item) { - const messageData = item.item as any; - if (messageData.id && !processedIds.has(messageData.id)) { - processedIds.add(messageData.id); - loadedMessages.push(messageData as Message); - } - } - // Handle direct Message structure (if item is already a message) - else if (item.role && item.message !== undefined) { - // This looks like a Message object directly - if (item.id && !processedIds.has(item.id)) { - processedIds.add(item.id); - loadedMessages.push(item as Message); - } - } - } - - console.log('[loadWorkflow] Loaded messages:', { - count: loadedMessages.length, - messageIds: loadedMessages.map(m => m.id), - sampleMessage: loadedMessages.length > 0 ? loadedMessages[0] : null - }); - - // Sort messages chronologically - const sortedMessages = loadedMessages.sort(sortMessages); - - // Update state - setMessages(sortedMessages); - setWorkflowId(workflowIdToLoad); - processedMessageIdsRef.current = processedIds; - - console.log('[loadWorkflow] State updated:', { - messagesCount: sortedMessages.length, - workflowId: workflowIdToLoad - }); - - // Don't refresh threads list here - it causes unwanted loading state - // Threads will be refreshed after new messages are sent/completed - } catch (err: any) { - console.error('Error loading workflow:', err); - setError(err.message || 'Failed to load workflow'); - } - }, [request]); - - // Select a thread (loads its messages) - const selectThread = useCallback(async (workflowIdToSelect: string) => { - setSelectedThreadId(workflowIdToSelect); - await loadWorkflow(workflowIdToSelect); - }, [loadWorkflow]); - - // Auto-load threads on initialization - useEffect(() => { - loadThreads(); - }, [loadThreads]); - - // Handle input change - const onInputChange = useCallback((value: string) => { - setInputValue(value); - }, []); - - // Handle file upload - const handleFileUpload = useCallback(async (file: File): Promise<{ success: boolean; data: any }> => { - setUploadError(null); - setUploadingFile(true); - - try { - // Validate file before upload - if (!file || !file.name || file.name.trim() === '') { - throw new Error('Invalid file: File must have a valid name'); - } - - if (file.size === 0) { - throw new Error('Invalid file: File cannot be empty'); - } - - const formData = new FormData(); - formData.append('file', file); - - const response = await api.post('/api/files/upload', formData, { - headers: { - 'Content-Type': 'multipart/form-data', - } - }); - - const fileData = response.data; - - // Extract fileId from response - // Backend returns: { message: "...", file: { id: "...", ... }, duplicateType: "..." } - const fileId = fileData?.file?.id || fileData?.id || fileData?.fileId; - - if (!fileId) { - console.error('Upload response structure:', fileData); - throw new Error('Upload failed: No file ID returned from server'); - } - - // Extract file name from response (use storedFileName if available, otherwise original fileName) - const fileName = fileData?.file?.fileName || fileData?.storedFileName || file.name; - - // Add to pending file IDs and uploaded files list - setPendingFileIds(prev => { - const updated = [...prev, fileId]; - pendingFileIdsRef.current = updated; // Keep ref in sync - return updated; - }); - setUploadedFiles(prev => [...prev, { fileId, fileName }]); - - return { success: true, data: fileData }; - } catch (err: any) { - console.error('File upload failed:', err); - const errorMessage = err.message || 'Failed to upload file'; - setUploadError(errorMessage); - return { success: false, data: null }; - } finally { - setUploadingFile(false); - } - }, []); - - // Handle file remove (remove from pending list) - const handleFileRemove = useCallback((fileId: string) => { - setPendingFileIds(prev => { - const updated = prev.filter(id => id !== fileId); - pendingFileIdsRef.current = updated; // Keep ref in sync - return updated; - }); - setUploadedFiles(prev => prev.filter(f => f.fileId !== fileId)); - }, []); - - // Stop chatbot workflow - const stopChatbot = useCallback(async () => { - if (!workflowId || !isRunning) { - return; - } - - try { - setIsSubmitting(true); - - // Abort any ongoing stream - if (streamAbortControllerRef.current) { - streamAbortControllerRef.current.abort(); - streamAbortControllerRef.current = null; - } - - // Clear thinking message when stopping - clearThinkingMessage(); - - // Call stop API - await stopChatbotApi(request, workflowId); - - setIsRunning(false); - setError(null); - } catch (err: any) { - console.error('Error stopping chatbot:', err); - setError(err.message || 'Failed to stop chatbot'); - } finally { - setIsSubmitting(false); - } - }, [workflowId, isRunning, request, clearThinkingMessage]); - - // Handle form submit - const handleSubmit = useCallback(async () => { - // If running, stop instead of starting - if (isRunning && workflowId) { - await stopChatbot(); - return; - } - - const trimmedInput = inputValue.trim(); - if (!trimmedInput || isSubmitting) { - return; - } - - try { - setIsSubmitting(true); - setError(null); - setIsRunning(true); - - // Abort any existing stream - if (streamAbortControllerRef.current) { - streamAbortControllerRef.current.abort(); - } - - // Create new abort controller for this stream - const abortController = new AbortController(); - streamAbortControllerRef.current = abortController; - - // Use ref to get current file IDs (avoids closure issues) - const fileIdsToSend = pendingFileIdsRef.current.length > 0 - ? pendingFileIdsRef.current - : pendingFileIds; // Fallback to state if ref is empty - - // Log for debugging - console.log('[handleSubmit] pendingFileIds from state:', pendingFileIds); - console.log('[handleSubmit] pendingFileIds from ref:', pendingFileIdsRef.current); - console.log('[handleSubmit] fileIdsToSend:', fileIdsToSend); - - const requestBody: StartChatbotRequest = { - prompt: trimmedInput, - userLanguage: 'en', - ...(workflowId && { workflowId }) - }; - - // Always include listFileId if there are any files - if (fileIdsToSend.length > 0) { - requestBody.listFileId = fileIdsToSend; - console.log('[handleSubmit] Added listFileId to requestBody:', fileIdsToSend); - } else { - console.warn('[handleSubmit] No file IDs to send! Check if files were uploaded correctly.'); - } - - console.log('[handleSubmit] Final requestBody:', JSON.stringify(requestBody, null, 2)); - - // Clear thinking message when starting a new request - clearThinkingMessage(); - processedLogsRef.current.clear(); // Clear processed logs for new request - - // Track if this is the first event (to reset isSubmitting) - let firstEventReceived = false; - - // Start SSE stream - await startChatbotStreamApi( - requestBody, - (item: ChatDataItem) => { - // Check if stream was aborted - if (abortController.signal.aborted) { - return; - } - - // Reset isSubmitting after first event to enable stop button - if (!firstEventReceived) { - firstEventReceived = true; - setIsSubmitting(false); - } - - // Process the chat data item - processChatDataItem(item); - - // Extract workflow ID from message if available - if (item.type === 'message' && item.item) { - const messageData = item.item as any; - if (messageData.workflowId) { - if (!workflowId) { - // New workflow created - select it automatically - setWorkflowId(messageData.workflowId); - setSelectedThreadId(messageData.workflowId); - } else { - // Existing workflow - ensure it's selected - setSelectedThreadId(messageData.workflowId); - } - } - } - }, - (err: Error) => { - // Only handle error if stream wasn't aborted - if (!abortController.signal.aborted) { - console.error('SSE stream error:', err); - setError(err.message || 'Stream error occurred'); - setIsRunning(false); - // Reset isSubmitting if stream fails before first event - if (!firstEventReceived) { - setIsSubmitting(false); - } - // Clear thinking messages on error - clearThinkingMessage(); - } else { - // Stream was aborted (stopped) - clear thinking messages - clearThinkingMessage(); - } - }, - () => { - // Stream completed - if (!abortController.signal.aborted) { - setIsRunning(false); - setInputValue(''); // Clear input on completion - // Clear pending file IDs after successful submission (files are now part of conversation) - setPendingFileIds([]); - pendingFileIdsRef.current = []; // Clear ref too - setUploadedFiles([]); - // Clear thinking message on completion (final message should have cleared it, but ensure cleanup) - clearThinkingMessage(); - // Refresh threads list after message completion (silently, without loading state) - setTimeout(() => { - loadThreadsSilently(); - }, 100); - } else { - // Stream was aborted (stopped) - clear thinking messages - clearThinkingMessage(); - } - } - ); - - // Clear input after starting (optimistic) - if (!workflowId) { - setInputValue(''); - } - } catch (err: any) { - console.error('Error starting chatbot:', err); - setError(err.message || 'Failed to start chatbot'); - setIsRunning(false); - // Clear thinking messages on error - clearThinkingMessage(); - } finally { - setIsSubmitting(false); - streamAbortControllerRef.current = null; - } - }, [inputValue, workflowId, isRunning, isSubmitting, stopChatbot, processChatDataItem, clearThinkingMessage, loadThreads, pendingFileIds]); - - // Delete a chatbot workflow - const handleDeleteThread = useCallback(async (workflowIdToDelete: string): Promise => { - try { - // Add to deleting set - setDeletingThreads(prev => new Set(prev).add(workflowIdToDelete)); - - // If deleting the selected thread, clear selection and messages - if (selectedThreadId === workflowIdToDelete) { - setMessages([]); - setWorkflowId(null); - setSelectedThreadId(null); - } - - // Call delete API - await deleteChatbotWorkflowApi(request, workflowIdToDelete); - - // Remove from threads list optimistically - setThreads(prev => prev.filter(t => t.id !== workflowIdToDelete)); - - return true; - } catch (err: any) { - console.error('Error deleting thread:', err); - throw err; - } finally { - // Remove from deleting set - setDeletingThreads(prev => { - const next = new Set(prev); - next.delete(workflowIdToDelete); - return next; - }); - } - }, [request, selectedThreadId]); - - // Optimistically remove thread from list - const removeThreadOptimistically = useCallback((workflowId: string) => { - setThreads(prev => prev.filter(t => t.id !== workflowId)); - // If deleting the selected thread, clear selection - if (selectedThreadId === workflowId) { - setSelectedThreadId(null); - setMessages([]); - setWorkflowId(null); - } - }, [selectedThreadId]); - - // Start a new chat (clears selection and messages) - const startNewChat = useCallback(() => { - // Abort any ongoing stream - if (streamAbortControllerRef.current) { - streamAbortControllerRef.current.abort(); - streamAbortControllerRef.current = null; - } - - // Clear messages and selection - setMessages([]); - setWorkflowId(null); - setSelectedThreadId(null); - setError(null); - setInputValue(''); - setPendingFileIds([]); - pendingFileIdsRef.current = []; - setUploadedFiles([]); - thinkingLogsRef.current = []; - thinkingMessageIdRef.current = null; - processedLogsRef.current.clear(); - clearProcessedMessages(); - }, [clearProcessedMessages]); - - // Reset chatbot state - const resetChatbot = useCallback(() => { - // Abort any ongoing stream - if (streamAbortControllerRef.current) { - streamAbortControllerRef.current.abort(); - streamAbortControllerRef.current = null; - } - - setMessages([]); - setWorkflowId(null); - setSelectedThreadId(null); - setIsRunning(false); - setIsSubmitting(false); - setError(null); - setInputValue(''); - setPendingFileIds([]); - pendingFileIdsRef.current = []; - setUploadedFiles([]); - thinkingLogsRef.current = []; - thinkingMessageIdRef.current = null; - processedLogsRef.current.clear(); - clearProcessedMessages(); - }, [clearProcessedMessages]); - - // Cleanup on unmount - const cleanup = useCallback(() => { - if (streamAbortControllerRef.current) { - streamAbortControllerRef.current.abort(); - streamAbortControllerRef.current = null; - } - logQueueRef.current = []; - isProcessingLogsRef.current = false; - processedLogsRef.current.clear(); - }, []); - - // Memoized display messages - const displayMessages = useMemo(() => { - return messages.sort(sortMessages); - }, [messages]); - - return { - // GenericDataHook interface - data: [], - loading: false, - error, - - // Input form interface - inputValue, - onInputChange, - handleSubmit, - isSubmitting, - - // Workflow state - workflowId: workflowId || undefined, - isRunning, - - // Messages - messages: displayMessages, - - // Chat history state - threads, - selectedThreadId, - threadsLoading, - threadsError, - - // Chat history methods - selectThread, - loadThreads, - - // Delete methods - handleDelete: handleDeleteThread, - removeOptimistically: removeThreadOptimistically, - deletingItems: deletingThreads, - refetch: loadThreads, - - // Additional methods - stopChatbot, - resetChatbot, - startNewChat, - cleanup, - - // File upload interface - handleFileUpload, - handleUpload: handleFileUpload, // Alias for compatibility with DragDropOverlay - handleFileRemove, - pendingFileIds, - uploadedFiles, - uploadingFile, - uploadError - }; -} - -export function createChatbotHook() { - return () => useChatbot(); -} - diff --git a/src/hooks/usePek.ts b/src/hooks/usePek.ts deleted file mode 100644 index 43d3723..0000000 --- a/src/hooks/usePek.ts +++ /dev/null @@ -1,991 +0,0 @@ -import { useState, useCallback, useEffect } from 'react'; -import api from '../api'; -import type { MapPoint, ParcelGeometry } from '../components/UiComponents/MapView'; -import { wgs84ToLV95 } from '../components/UiComponents/MapView/LV95Converter'; - -// Parcel search response interfaces -export interface ParcelSearchResponse { - parcel: { - id: string; - egrid?: string; - number?: string; - name?: string; - identnd?: string; - canton?: string; - municipality_code?: number; - municipality_name?: string; - address?: string; - perimeter?: { - closed: boolean; - punkte: Array<{ - koordinatensystem: string; - x: number; - y: number; - z: number | null; - }>; - }; - area_m2?: number; - centroid?: { x: number; y: number }; - geoportal_url?: string; - realestate_type?: string | null; - bauzone?: string | null; - zone?: Array | null; - }; - map_view: { - center: { x: number; y: number }; - zoom_bounds: { - min_x: number; - min_y: number; - max_x: number; - max_y: number; - }; - geometry_geojson: { - type: string; - geometry: { - type: string; - coordinates: number[][][]; - }; - properties: { - id: string; - egrid?: string; - number?: string; - }; - }; - }; - adjacent_parcels?: Array<{ - id: string; - egrid?: string; - number?: string; - perimeter?: { - closed: boolean; - punkte: Array<{ - koordinatensystem: string; - x: number; - y: number; - z: number | null; - }>; - }; - geometry_geojson?: { - type: string; - geometry: { - type: string; - coordinates: number[][][]; - }; - properties: { - id: string; - egrid?: string; - number?: string; - }; - }; - }>; -} - -// Command response interface -export interface CommandResponse { - success: boolean; - intent?: string; - entity?: string; - result?: any; - error?: string; -} - -// Project interfaces -export interface Projekt { - id: string; - mandateId?: string; - label: string; - statusProzess?: string; - perimeter?: any; - baulinie?: any; - parzellen?: any[]; - dokumente?: any[]; - kontextInformationen?: any[]; -} - -export interface CreateProjektResponse { - projekt: Projekt; - parzellen?: any[]; -} - -export interface AddParcelResponse { - projekt: Projekt; - parzelle: any; -} - -// Main PEK hook -export function usePek() { - // Location input state - separate fields - const [kanton, setKanton] = useState(''); - const [gemeinde, setGemeinde] = useState(''); - const [adresse, setAdresse] = useState(''); - const [isGettingLocation, setIsGettingLocation] = useState(false); - const [locationError, setLocationError] = useState(null); - - // Legacy locationInput for backward compatibility (combines fields) - const locationInput = [kanton, gemeinde, adresse].filter(Boolean).join(', '); - const setLocationInput = (value: string) => { - // Parse combined input if needed (for map clicks, etc.) - const parts = value.split(',').map(p => p.trim()); - if (parts.length >= 3) { - setKanton(parts[0]); - setGemeinde(parts[1]); - setAdresse(parts.slice(2).join(', ')); - } else { - setAdresse(value); - } - }; - - // Parcel search state - const [selectedParcels, setSelectedParcels] = useState([]); - const [isSearchingParcel, setIsSearchingParcel] = useState(false); - const [parcelSearchError, setParcelSearchError] = useState(null); - - // Map view state - const [mapCenter, setMapCenter] = useState(null); - const [mapZoomBounds, setMapZoomBounds] = useState<{ - min_x: number; - min_y: number; - max_x: number; - max_y: number; - } | null>(null); - const [parcelGeometries, setParcelGeometries] = useState([]); - - // Command processing state - const [commandInput, setCommandInput] = useState(''); - const [isProcessingCommand, setIsProcessingCommand] = useState(false); - const [commandResults, setCommandResults] = useState([]); - const [commandError, setCommandError] = useState(null); - - // Project state - const [currentProjekt, setCurrentProjekt] = useState(null); - const [isCreatingProjekt, setIsCreatingProjekt] = useState(false); - const [isAddingParcel, setIsAddingParcel] = useState(false); - const [projektError, setProjektError] = useState(null); - - // Panel state - const [isPanelOpen, setIsPanelOpen] = useState(false); - - // Update parcel geometries when selected parcels change - // Ensure all selected parcels are marked as selected and not as adjacent - useEffect(() => { - const selectedParcelIds = new Set(selectedParcels.map(p => p.parcel.id)); - - setParcelGeometries(prev => prev.map(geo => { - const isSelected = selectedParcelIds.has(geo.id); - // If parcel is selected, it should not be marked as adjacent - const isAdjacent = isSelected ? false : geo.isAdjacent; - return { ...geo, isSelected, isAdjacent }; - })); - }, [selectedParcels]); - - /** - * Get current geolocation and directly search for parcel - * Does not fill input fields, directly makes the request - */ - const useCurrentLocation = useCallback(async () => { - setIsGettingLocation(true); - setLocationError(null); - - try { - if (!navigator.geolocation) { - throw new Error('Geolocation wird von Ihrem Browser nicht unterstützt'); - } - - return new Promise((resolve, reject) => { - navigator.geolocation.getCurrentPosition( - async (position) => { - try { - // Convert WGS84 to LV95 using the converter function - const lat = position.coords.latitude; - const lon = position.coords.longitude; - - const lv95 = wgs84ToLV95(lat, lon); - const locationString = `${Math.round(lv95.x)},${Math.round(lv95.y)}`; - - // Directly search for parcel without updating input fields - await searchParcel(locationString, true); - resolve(); - } catch (err: any) { - setLocationError(err.message || 'Fehler beim Konvertieren der Koordinaten'); - reject(err); - } - }, - (error) => { - let errorMessage = 'Fehler beim Abrufen der Position'; - switch (error.code) { - case error.PERMISSION_DENIED: - errorMessage = 'Zugriff auf Standort wurde verweigert'; - break; - case error.POSITION_UNAVAILABLE: - errorMessage = 'Standortinformationen nicht verfügbar'; - break; - case error.TIMEOUT: - errorMessage = 'Zeitüberschreitung beim Abrufen der Position'; - break; - } - setLocationError(errorMessage); - reject(new Error(errorMessage)); - }, - { - enableHighAccuracy: true, - timeout: 10000, - maximumAge: 0 - } - ); - }); - } catch (err: any) { - setLocationError(err.message || 'Fehler beim Abrufen der aktuellen Position'); - throw err; - } finally { - setIsGettingLocation(false); - } - }, []); - - /** - * Search for parcel by location (address or coordinates) - * Always includes adjacent parcels by default - */ - const searchParcel = useCallback(async (location: string, includeAdjacent: boolean = true) => { - if (!location.trim()) { - setParcelSearchError('Bitte geben Sie einen Standort ein'); - return; - } - - setIsSearchingParcel(true); - setParcelSearchError(null); - - try { - const response = await api.get('/api/realestate/parcel/search', { - params: { - location: location.trim(), - include_adjacent: includeAdjacent - } - }); - - const data: ParcelSearchResponse = response.data; - - // Debug logging - if (import.meta.env.DEV) { - console.log('📦 Parcel search response:', { - hasMapView: !!data.map_view, - hasGeometry: !!data.map_view?.geometry_geojson, - hasPerimeter: !!data.parcel.perimeter, - adjacentCount: data.adjacent_parcels?.length || 0 - }); - } - - // Add parcel to selected parcels array if not already selected - // Update geometries within the callback to have access to updated selectedParcels - setSelectedParcels(prev => { - const exists = prev.some(p => p.parcel.id === data.parcel.id); - if (exists) { - return prev; // Already selected, don't add again - } - - const updatedSelectedParcels = [...prev, data]; - const selectedParcelIds = new Set(updatedSelectedParcels.map(p => p.parcel.id)); - - // Update geometries - setParcelGeometries(currentGeometries => { - const geometryMap = new Map(); - - // Keep existing geometries - currentGeometries.forEach(geo => { - geometryMap.set(geo.id, geo); - }); - - // Update map center and zoom bounds - if (data.map_view) { - setMapCenter(data.map_view.center); - setMapZoomBounds(data.map_view.zoom_bounds); - - // Main parcel - use geometry_geojson if available, otherwise use perimeter.punkte - let mainParcelCoordinates: MapPoint[] = []; - - if (data.map_view.geometry_geojson?.geometry?.coordinates) { - const coords = data.map_view.geometry_geojson.geometry.coordinates[0]; - if (Array.isArray(coords)) { - mainParcelCoordinates = coords.map((coord: number[]) => ({ - x: coord[0], - y: coord[1] - })); - } - } else if (data.parcel.perimeter?.punkte) { - mainParcelCoordinates = data.parcel.perimeter.punkte.map((p) => ({ - x: p.x, - y: p.y - })); - } - - if (mainParcelCoordinates.length > 0) { - geometryMap.set(data.parcel.id, { - id: data.parcel.id, - egrid: data.parcel.egrid, - number: data.parcel.number, - coordinates: mainParcelCoordinates, - isSelected: true, - isAdjacent: false - }); - } - - // Add adjacent parcels, but skip if already selected - if (data.adjacent_parcels && includeAdjacent && data.adjacent_parcels.length > 0) { - data.adjacent_parcels.forEach((adjacent) => { - // Skip if this adjacent parcel is already selected - if (selectedParcelIds.has(adjacent.id)) { - // If it exists, mark as selected, not adjacent - const existingGeo = geometryMap.get(adjacent.id); - if (existingGeo) { - geometryMap.set(adjacent.id, { - ...existingGeo, - isSelected: true, - isAdjacent: false - }); - } - if (import.meta.env.DEV) { - console.log(`⏭️ Skipping adjacent parcel ${adjacent.id} - already selected`); - } - return; - } - - // Only add if not already in map - if (!geometryMap.has(adjacent.id)) { - let adjCoordinates: MapPoint[] = []; - - if (adjacent.geometry_geojson?.geometry?.coordinates) { - const coords = adjacent.geometry_geojson.geometry.coordinates[0]; - if (Array.isArray(coords) && coords.length > 0) { - adjCoordinates = coords.map((coord: number[]) => ({ - x: coord[0], - y: coord[1] - })); - } - } else if (adjacent.perimeter?.punkte) { - adjCoordinates = adjacent.perimeter.punkte.map((p) => ({ - x: p.x, - y: p.y - })); - } - - if (adjCoordinates.length >= 3) { - geometryMap.set(adjacent.id, { - id: adjacent.id, - egrid: adjacent.egrid, - number: adjacent.number, - coordinates: adjCoordinates, - isSelected: false, - isAdjacent: true - }); - } - } - }); - } - } else { - // If no map_view, still try to use parcel data - if (data.parcel.perimeter?.punkte) { - const coordinates = data.parcel.perimeter.punkte.map((p) => ({ - x: p.x, - y: p.y - })); - - geometryMap.set(data.parcel.id, { - id: data.parcel.id, - egrid: data.parcel.egrid, - number: data.parcel.number, - coordinates, - isSelected: true, - isAdjacent: false - }); - - if (data.parcel.centroid) { - setMapCenter(data.parcel.centroid); - } - } - } - - // Update all geometries: mark selected ones and unmark adjacent for selected ones - const updatedGeometries = Array.from(geometryMap.values()).map(geo => { - const isSelected = selectedParcelIds.has(geo.id); - return { - ...geo, - isSelected, - isAdjacent: isSelected ? false : geo.isAdjacent - }; - }); - - if (import.meta.env.DEV) { - console.log(`🗺️ Total geometries to display: ${updatedGeometries.length}`, { - selected: updatedGeometries.filter(g => g.isSelected).length, - adjacent: updatedGeometries.filter(g => g.isAdjacent).length - }); - } - - return updatedGeometries; - }); - - return updatedSelectedParcels; - }); - - // Open panel when parcel is found - setIsPanelOpen(true); - - return { success: true, data }; - } catch (err: any) { - const errorMessage = - err.response?.data?.detail || err.message || 'Fehler beim Suchen der Parzelle'; - setParcelSearchError(errorMessage); - return { success: false, error: errorMessage }; - } finally { - setIsSearchingParcel(false); - } - }, []); - - /** - * Handle map click - search for parcel at clicked coordinates - */ - const handleMapClick = useCallback( - async (point: MapPoint) => { - const locationString = `${point.x},${point.y}`; - setLocationInput(locationString); - await searchParcel(locationString, true); // Always include adjacent parcels - }, - [searchParcel] - ); - - /** - * Check if a parcel is selected - */ - const isParcelSelected = useCallback((parcelId: string): boolean => { - return selectedParcels.some(p => p.parcel.id === parcelId); - }, [selectedParcels]); - - /** - * Remove a parcel from selection - */ - const removeParcel = useCallback((parcelId: string) => { - setSelectedParcels(prev => prev.filter(p => p.parcel.id !== parcelId)); - // Update geometries to reflect deselection - setParcelGeometries(prev => prev.map(geo => - geo.id === parcelId ? { ...geo, isSelected: false } : geo - )); - }, []); - - /** - * Clear all selected parcels - */ - const clearSelectedParcels = useCallback(() => { - setSelectedParcels([]); - // Update geometries to reflect deselection - setParcelGeometries(prev => prev.map(geo => ({ ...geo, isSelected: false }))); - }, []); - - /** - * Handle parcel click on map - toggle parcel selection - */ - const handleParcelClick = useCallback(async (parcelId: string) => { - // Check if parcel is already selected - const isSelected = isParcelSelected(parcelId); - - if (isSelected) { - // Remove from selection - removeParcel(parcelId); - } else { - // Find the clicked parcel in the geometries - const clickedParcel = parcelGeometries.find(p => p.id === parcelId); - - if (clickedParcel && clickedParcel.coordinates.length > 0) { - // Use a point inside the parcel (first coordinate is always on the boundary, which is inside) - const firstCoord = clickedParcel.coordinates[0]; - - // Use first coordinate (guaranteed to be on/in the parcel) for search - const locationString = `${firstCoord.x},${firstCoord.y}`; - await searchParcel(locationString, true); // Always include adjacent parcels - } else { - // Fallback: try to search by parcel ID/EGRID if available - // Check all selected parcels for adjacent parcels - for (const selectedParcel of selectedParcels) { - if (selectedParcel.adjacent_parcels) { - const adjacentParcel = selectedParcel.adjacent_parcels.find(p => p.id === parcelId); - if (adjacentParcel?.egrid) { - // Search by EGRID - await searchParcel(adjacentParcel.egrid, true); - break; - } else if (adjacentParcel?.number) { - // Try searching by number (might need address context) - await searchParcel(adjacentParcel.number, true); - break; - } else if (adjacentParcel?.id) { - // Last resort: try searching by ID - await searchParcel(adjacentParcel.id, true); - break; - } - } - } - } - } - }, [parcelGeometries, selectedParcels, isParcelSelected, removeParcel, searchParcel]); - - /** - * Process natural language command - * Always includes the currently selected parcel if available - */ - const processCommand = useCallback(async (userInput: string) => { - if (!userInput.trim()) { - setCommandError('Bitte geben Sie einen Befehl ein'); - return; - } - - setIsProcessingCommand(true); - setCommandError(null); - - // Add user message - const userMessage = { - id: `user-${Date.now()}`, - role: 'user', - message: userInput.trim(), - timestamp: Date.now() - }; - setCommandResults((prev) => [...prev, userMessage]); - - try { - // Build request body with user input and selected parcel - const requestBody: any = { - userInput: userInput.trim() - }; - - // Always include the currently selected parcels if available - if (selectedParcels.length > 0) { - // Use first selected parcel for backward compatibility - const firstParcel = selectedParcels[0]; - requestBody.selectedParcel = { - id: firstParcel.parcel.id, - egrid: firstParcel.parcel.egrid, - number: firstParcel.parcel.number, - name: firstParcel.parcel.name, - identnd: firstParcel.parcel.identnd, - canton: firstParcel.parcel.canton, - municipality_code: firstParcel.parcel.municipality_code, - municipality_name: firstParcel.parcel.municipality_name, - address: firstParcel.parcel.address, - area_m2: firstParcel.parcel.area_m2, - centroid: firstParcel.parcel.centroid, - geoportal_url: firstParcel.parcel.geoportal_url, - realestate_type: firstParcel.parcel.realestate_type, - bauzone: firstParcel.parcel.bauzone, - zone: firstParcel.parcel.zone, - // Include geometry data if available - geometry_geojson: firstParcel.map_view?.geometry_geojson, - perimeter: firstParcel.parcel.perimeter - }; - // Also include all selected parcels as array - requestBody.selectedParcels = selectedParcels.map(p => ({ - id: p.parcel.id, - egrid: p.parcel.egrid, - number: p.parcel.number, - name: p.parcel.name, - identnd: p.parcel.identnd, - canton: p.parcel.canton, - municipality_code: p.parcel.municipality_code, - municipality_name: p.parcel.municipality_name, - address: p.parcel.address, - area_m2: p.parcel.area_m2, - centroid: p.parcel.centroid, - geoportal_url: p.parcel.geoportal_url, - realestate_type: p.parcel.realestate_type, - bauzone: p.parcel.bauzone, - zone: p.parcel.zone, - geometry_geojson: p.map_view?.geometry_geojson, - perimeter: p.parcel.perimeter - })); - } - - const response = await api.post('/api/realestate/command', requestBody); - - const data: CommandResponse = response.data; - - // Format response as assistant message - let responseMessage = ''; - if (data.success) { - if (data.result) { - if (typeof data.result === 'object') { - responseMessage = `**Intent:** ${data.intent || 'Unknown'}\n**Entity:** ${data.entity || 'N/A'}\n\n**Result:**\n\`\`\`json\n${JSON.stringify(data.result, null, 2)}\n\`\`\``; - } else { - responseMessage = `**Intent:** ${data.intent || 'Unknown'}\n**Entity:** ${data.entity || 'N/A'}\n\n**Result:** ${data.result}`; - } - } else { - responseMessage = `Command executed successfully.\n**Intent:** ${data.intent || 'Unknown'}\n**Entity:** ${data.entity || 'N/A'}`; - } - } else { - responseMessage = `Error: ${data.error || 'Unknown error'}`; - } - - const assistantMessage = { - id: `assistant-${Date.now()}`, - role: 'assistant', - message: responseMessage, - timestamp: Date.now() - }; - setCommandResults((prev) => [...prev, assistantMessage]); - - // If a project was created and there are selected parcels, automatically add them - if (data.success && data.intent === 'CREATE' && data.entity === 'Projekt' && selectedParcels.length > 0) { - try { - // Extract projekt from result - const projektResult = data.result?.result || data.result; - if (projektResult?.id) { - // Set as current projekt - setCurrentProjekt(projektResult); - - // Add all selected parcels to the newly created project via direct API call - let addedCount = 0; - for (const selectedParcel of selectedParcels) { - try { - const addParcelRequestBody: any = { - parcelId: selectedParcel.parcel.id, - parcelData: { - id: selectedParcel.parcel.id, - egrid: selectedParcel.parcel.egrid, - number: selectedParcel.parcel.number, - name: selectedParcel.parcel.name, - identnd: selectedParcel.parcel.identnd, - canton: selectedParcel.parcel.canton, - municipality_code: selectedParcel.parcel.municipality_code, - municipality_name: selectedParcel.parcel.municipality_name, - address: selectedParcel.parcel.address, - area_m2: selectedParcel.parcel.area_m2, - centroid: selectedParcel.parcel.centroid, - geoportal_url: selectedParcel.parcel.geoportal_url, - realestate_type: selectedParcel.parcel.realestate_type, - bauzone: selectedParcel.parcel.bauzone, - zone: selectedParcel.parcel.zone, - geometry_geojson: selectedParcel.map_view?.geometry_geojson, - perimeter: selectedParcel.parcel.perimeter - } - }; - - const addResponse = await api.post( - `/api/realestate/projekt/${projektResult.id}/add-parcel`, - addParcelRequestBody - ); - const addResult: AddParcelResponse = addResponse.data; - - // Update current projekt with the updated version that includes the parcel - setCurrentProjekt(addResult.projekt); - addedCount++; - } catch (addError: any) { - console.error(`Failed to add parcel ${selectedParcel.parcel.id} to project:`, addError); - } - } - - // Update the assistant message to indicate parcels were added - const parcelText = addedCount === 1 ? 'Parzelle' : 'Parzellen'; - const updateMessage = { - ...assistantMessage, - id: `assistant-update-${Date.now()}`, - message: `${responseMessage}\n\n✅ ${addedCount} ${parcelText} ${addedCount === 1 ? 'wurde' : 'wurden'} automatisch zum Projekt hinzugefügt.` - }; - setCommandResults((prev) => { - const updated = [...prev]; - const lastIndex = updated.length - 1; - if (updated[lastIndex]?.id === assistantMessage.id) { - updated[lastIndex] = updateMessage; - } - return updated; - }); - } - } catch (addError: any) { - // Log error but don't fail the command - console.error('Failed to automatically add parcel to project:', addError); - const errorMessage = addError.response?.data?.detail || addError.message || 'Unbekannter Fehler'; - const errorUpdate = { - id: `assistant-error-${Date.now()}`, - role: 'assistant', - message: `⚠️ Projekt wurde erstellt, aber Parzelle konnte nicht automatisch hinzugefügt werden: ${errorMessage}`, - timestamp: Date.now() - }; - setCommandResults((prev) => [...prev, errorUpdate]); - } - } - - // If a parcel was created and there are selected parcels, automatically populate it with the first selected parcel data - if (data.success && data.intent === 'CREATE' && data.entity === 'Parzelle' && selectedParcels.length > 0) { - const selectedParcel = selectedParcels[0]; // Use first selected parcel - try { - // Extract parzelle from result - const parzelleResult = data.result?.result || data.result; - if (parzelleResult?.id) { - // Update the newly created parcel with data from the selected parcel - const updateParcelRequestBody: any = { - // Map selected parcel data to parzelle fields - egrid: selectedParcel.parcel.egrid, - number: selectedParcel.parcel.number, - name: selectedParcel.parcel.name, - identnd: selectedParcel.parcel.identnd, - canton: selectedParcel.parcel.canton, - municipality_code: selectedParcel.parcel.municipality_code, - municipality_name: selectedParcel.parcel.municipality_name, - address: selectedParcel.parcel.address, - strasseNr: selectedParcel.parcel.address, - area_m2: selectedParcel.parcel.area_m2, - centroid: selectedParcel.parcel.centroid, - geoportal_url: selectedParcel.parcel.geoportal_url, - realestate_type: selectedParcel.parcel.realestate_type, - bauzone: selectedParcel.parcel.bauzone, - zone: selectedParcel.parcel.zone, - // Include geometry data - geometry_geojson: selectedParcel.map_view?.geometry_geojson, - perimeter: selectedParcel.parcel.perimeter - }; - - // Try to update the parcel via PUT request - try { - await api.put( - `/api/realestate/parzelle/${parzelleResult.id}`, - updateParcelRequestBody - ); - - // Update the assistant message to indicate parcel was populated - const updateMessage = { - ...assistantMessage, - id: `assistant-update-${Date.now()}`, - message: `${responseMessage}\n\n✅ Parzelle wurde automatisch mit Daten der Kartenauswahl befüllt.` - }; - setCommandResults((prev) => { - const updated = [...prev]; - const lastIndex = updated.length - 1; - if (updated[lastIndex]?.id === assistantMessage.id) { - updated[lastIndex] = updateMessage; - } - return updated; - }); - } catch (putError: any) { - // If PUT doesn't work, try PATCH - try { - await api.patch( - `/api/realestate/parzelle/${parzelleResult.id}`, - updateParcelRequestBody - ); - - const updateMessage = { - ...assistantMessage, - id: `assistant-update-${Date.now()}`, - message: `${responseMessage}\n\n✅ Parzelle wurde automatisch mit Daten der Kartenauswahl befüllt.` - }; - setCommandResults((prev) => { - const updated = [...prev]; - const lastIndex = updated.length - 1; - if (updated[lastIndex]?.id === assistantMessage.id) { - updated[lastIndex] = updateMessage; - } - return updated; - }); - } catch (patchError: any) { - // If both PUT and PATCH fail, log but don't fail the command - console.error('Failed to update parcel with selected parcel data:', patchError); - const errorMessage = patchError.response?.data?.detail || patchError.message || 'Unbekannter Fehler'; - const errorUpdate = { - id: `assistant-error-${Date.now()}`, - role: 'assistant', - message: `⚠️ Parzelle wurde erstellt, aber konnte nicht automatisch mit Kartenauswahl-Daten befüllt werden: ${errorMessage}`, - timestamp: Date.now() - }; - setCommandResults((prev) => [...prev, errorUpdate]); - } - } - } - } catch (updateError: any) { - // Log error but don't fail the command - console.error('Failed to automatically populate parcel with selected parcel data:', updateError); - const errorMessage = updateError.response?.data?.detail || updateError.message || 'Unbekannter Fehler'; - const errorUpdate = { - id: `assistant-error-${Date.now()}`, - role: 'assistant', - message: `⚠️ Parzelle wurde erstellt, aber konnte nicht automatisch mit Kartenauswahl-Daten befüllt werden: ${errorMessage}`, - timestamp: Date.now() - }; - setCommandResults((prev) => [...prev, errorUpdate]); - } - } - - // Clear input on success - setCommandInput(''); - - return { success: true, data }; - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Failed to send command'; - setCommandError(errorMessage); - - // Add error message - const errorMsg = { - id: `error-${Date.now()}`, - role: 'assistant', - message: `**Error:** ${errorMessage}`, - timestamp: Date.now() - }; - setCommandResults((prev) => [...prev, errorMsg]); - - return { success: false, error: errorMessage }; - } finally { - setIsProcessingCommand(false); - } - }, [selectedParcels]); - - /** - * Create a new project - */ - const createProjekt = useCallback( - async (data: { - label: string; - statusProzess?: string; - location?: string; - parcelIds?: string[]; - }) => { - setIsCreatingProjekt(true); - setProjektError(null); - - try { - const requestBody: any = { - label: data.label - }; - - if (data.statusProzess) { - requestBody.statusProzess = data.statusProzess; - } - - if (data.location) { - requestBody.location = data.location; - } - - if (data.parcelIds && data.parcelIds.length > 0) { - requestBody.parcelIds = data.parcelIds; - } - - const response = await api.post('/api/realestate/projekt/create', requestBody); - const result: CreateProjektResponse = response.data; - - setCurrentProjekt(result.projekt); - return { success: true, data: result }; - } catch (err: any) { - const errorMessage = - err.response?.data?.detail || err.message || 'Fehler beim Erstellen des Projekts'; - setProjektError(errorMessage); - return { success: false, error: errorMessage }; - } finally { - setIsCreatingProjekt(false); - } - }, - [] - ); - - /** - * Add a parcel to an existing project - */ - const addParcelToProjekt = useCallback( - async ( - projektId: string, - data: { - parcelId?: string; - location?: string; - parcelData?: Record; - } - ) => { - if (!currentProjekt || currentProjekt.id !== projektId) { - setProjektError('Projekt nicht gefunden'); - return { success: false, error: 'Projekt nicht gefunden' }; - } - - setIsAddingParcel(true); - setProjektError(null); - - try { - const requestBody: any = {}; - - if (data.parcelId) { - requestBody.parcelId = data.parcelId; - } else if (data.location) { - requestBody.location = data.location; - } else if (data.parcelData) { - requestBody.parcelData = data.parcelData; - } else { - throw new Error('Bitte geben Sie parcelId, location oder parcelData an'); - } - - const response = await api.post( - `/api/realestate/projekt/${projektId}/add-parcel`, - requestBody - ); - const result: AddParcelResponse = response.data; - - setCurrentProjekt(result.projekt); - return { success: true, data: result }; - } catch (err: any) { - const errorMessage = - err.response?.data?.detail || err.message || 'Fehler beim Hinzufügen der Parzelle'; - setProjektError(errorMessage); - return { success: false, error: errorMessage }; - } finally { - setIsAddingParcel(false); - } - }, - [currentProjekt] - ); - - // Build location string from separate fields - const buildLocationString = useCallback(() => { - const parts = [kanton, gemeinde, adresse].filter(Boolean); - return parts.join(', '); - }, [kanton, gemeinde, adresse]); - - return { - // Location input - separate fields - kanton, - setKanton, - gemeinde, - setGemeinde, - adresse, - setAdresse, - buildLocationString, - // Legacy locationInput for backward compatibility - locationInput, - setLocationInput, - useCurrentLocation, - isGettingLocation, - locationError, - - // Parcel search - selectedParcels, - searchParcel, - isSearchingParcel, - parcelSearchError, - removeParcel, - clearSelectedParcels, - isParcelSelected, - - // Map view - mapCenter, - mapZoomBounds, - parcelGeometries, - handleMapClick, - handleParcelClick, - - // Command processing - commandInput, - setCommandInput, - processCommand, - isProcessingCommand, - commandResults, - commandError, - - // Project management - currentProjekt, - createProjekt, - isCreatingProjekt, - addParcelToProjekt, - isAddingParcel, - projektError, - - // Panel state - isPanelOpen, - setIsPanelOpen - }; -} diff --git a/src/hooks/usePekTables.ts b/src/hooks/usePekTables.ts deleted file mode 100644 index 2661ce4..0000000 --- a/src/hooks/usePekTables.ts +++ /dev/null @@ -1,1184 +0,0 @@ -import { useState, useCallback, useEffect } from 'react'; -import api from '../api'; -import type { GenericDataHook } from '../core/PageManager/pageInterface'; -import { fetchAttributes } from '../api/attributesApi'; -import { useApiRequest } from './useApi'; -import { usePermissions, type UserPermissions } from './usePermissions'; -import { - isDateTimeType, - isSelectType, - isMultiselectType, - isCheckboxType, - isTextareaType, - type AttributeType -} from '../utils/attributeTypeMapper'; - -// Table list response interface -export interface TableInfo { - name: string; - description: string; - model: string; -} - -export interface TablesListResponse { - tables: TableInfo[]; - count: number; -} - -// Command response interface -export interface CommandResponse { - success: boolean; - intent: string; - entity: string; - result: any; - message?: string; -} - -// Table data response interface -export interface TableDataResponse { - items: any[]; - pagination: { - currentPage: number; - pageSize: number; - totalItems: number; - totalPages: number; - sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; - filters?: any; - } | null; -} - -// Message interface for chat -export interface PekMessage { - id: string; - role: 'user' | 'assistant'; - message: string; - timestamp: Date; - data?: any; -} - -export function usePekTables() { - // Tables list state - initialize as empty array - const [tables, setTables] = useState([]); - const [isLoadingTables, setIsLoadingTables] = useState(false); - const [tablesError, setTablesError] = useState(null); - - // Selected table state - const [selectedTable, setSelectedTable] = useState(''); - const [tableData, setTableData] = useState([]); - const [isLoadingTableData, setIsLoadingTableData] = useState(false); - const [tableDataError, setTableDataError] = useState(null); - const [pagination, setPagination] = useState(null); - - // Command state - const [commandInput, setCommandInput] = useState(''); - const [isProcessingCommand, setIsProcessingCommand] = useState(false); - const [commandError, setCommandError] = useState(null); - const [messages, setMessages] = useState([]); - - /** - * Load list of available tables - */ - const loadTables = useCallback(async () => { - setIsLoadingTables(true); - setTablesError(null); - - try { - const response = await api.get('/api/realestate/tables'); - const data: TablesListResponse = response.data; - setTables(data.tables || []); - - // Auto-select first table if available - if (data.tables && data.tables.length > 0 && !selectedTable) { - setSelectedTable(data.tables[0].model); - } - } catch (err: any) { - const errorMessage = - err.response?.data?.detail || err.message || 'Fehler beim Laden der Tabellen'; - setTablesError(errorMessage); - } finally { - setIsLoadingTables(false); - } - }, [selectedTable]); - - /** - * Load data for a specific table - */ - const loadTableData = useCallback(async (tableName: string, page?: number, pageSize?: number, sort?: Array<{ field: string; direction: 'asc' | 'desc' }>) => { - setIsLoadingTableData(true); - setTableDataError(null); - - try { - const params: any = {}; - - // Build pagination object if provided - if (page !== undefined || pageSize !== undefined || sort) { - params.pagination = JSON.stringify({ - page: page || 1, - pageSize: pageSize || 10, - sort: sort || [] - }); - } - - const response = await api.get(`/api/realestate/table/${tableName}`, { params }); - const data: TableDataResponse = response.data; - - setTableData(data.items || []); - setPagination(data.pagination); - } catch (err: any) { - const errorMessage = - err.response?.data?.detail || err.message || `Fehler beim Laden der Tabelle ${tableName}`; - setTableDataError(errorMessage); - } finally { - setIsLoadingTableData(false); - } - }, []); - - // Load tables list on mount - useEffect(() => { - loadTables(); - }, [loadTables]); - - // Load table data when selected table changes - useEffect(() => { - if (selectedTable) { - loadTableData(selectedTable); - } else { - setTableData([]); - setPagination(null); - } - }, [selectedTable, loadTableData]); - - /** - * Process natural language command - */ - const processCommand = useCallback(async (userInput: string) => { - if (!userInput.trim()) { - setCommandError('Bitte geben Sie einen Befehl ein.'); - return; - } - - setIsProcessingCommand(true); - setCommandError(null); - - // Add user message - const userMessage: PekMessage = { - id: `user-${Date.now()}`, - role: 'user', - message: userInput, - timestamp: new Date() - }; - setMessages((prev) => [...prev, userMessage]); - - try { - const response = await api.post('/api/realestate/command', { - userInput: userInput.trim() - }); - - const data: CommandResponse = response.data; - - // Add assistant message - const assistantMessage: PekMessage = { - id: `assistant-${Date.now()}`, - role: 'assistant', - message: data.message || `Befehl erfolgreich ausgeführt: ${data.intent} ${data.entity}`, - timestamp: new Date(), - data: data.result - }; - setMessages((prev) => [...prev, assistantMessage]); - - // Reload table data if command might have affected it - if (selectedTable && (data.intent === 'CREATE' || data.intent === 'UPDATE' || data.intent === 'DELETE')) { - await loadTableData(selectedTable); - } - - // Clear input - setCommandInput(''); - - return { success: true, data }; - } catch (err: any) { - const errorMessage = - err.response?.data?.detail || err.message || 'Fehler beim Verarbeiten des Befehls'; - setCommandError(errorMessage); - - // Add error message - const errorMsg: PekMessage = { - id: `error-${Date.now()}`, - role: 'assistant', - message: `Fehler: ${errorMessage}`, - timestamp: new Date() - }; - setMessages((prev) => [...prev, errorMsg]); - - return { success: false, error: errorMessage }; - } finally { - setIsProcessingCommand(false); - } - }, [selectedTable, loadTableData]); - - /** - * Create a new record in the selected table - */ - const createRecord = useCallback(async (tableName: string, data: any) => { - try { - const response = await api.post(`/api/realestate/table/${tableName}`, data); - - // Reload table data - if (tableName === selectedTable) { - await loadTableData(selectedTable); - } - - return { success: true, data: response.data }; - } catch (err: any) { - const errorMessage = - err.response?.data?.detail || err.message || 'Fehler beim Erstellen des Eintrags'; - return { success: false, error: errorMessage }; - } - }, [selectedTable, loadTableData]); - - /** - * Refresh current table data - */ - const refreshTableData = useCallback(() => { - if (selectedTable) { - loadTableData(selectedTable); - } - }, [selectedTable, loadTableData]); - - return { - // Tables list - tables, - isLoadingTables, - tablesError, - loadTables, - - // Selected table - selectedTable, - setSelectedTable, - tableData, - isLoadingTableData, - tableDataError, - pagination, - loadTableData, - refreshTableData, - - // Commands - commandInput, - setCommandInput, - processCommand, - isProcessingCommand, - commandError, - messages, - - // Create record - createRecord - }; -} - -/** - * Hook factory that creates a hook for a specific table model - * Returns a hook function compatible with GenericDataHook interface - */ -export function createPekTableHook(tableModel: string): () => GenericDataHook { - return () => { - const [tableData, setTableData] = useState([]); - const [isLoadingTableData, setIsLoadingTableData] = useState(false); - const [tableDataError, setTableDataError] = useState(null); - const [pagination, setPagination] = useState(null); - - /** - * Load data for the specific table - */ - const loadTableData = useCallback(async ( - page?: number, - pageSize?: number, - sort?: Array<{ field: string; direction: 'asc' | 'desc' }>, - filters?: any, - search?: string - ) => { - setIsLoadingTableData(true); - setTableDataError(null); - - try { - const params: any = {}; - - // Build pagination object - const paginationObj: any = { - page: page || 1, - pageSize: pageSize || 10, - sort: sort || [] - }; - - if (filters) { - paginationObj.filters = filters; - } - - if (search) { - paginationObj.search = search; - } - - params.pagination = JSON.stringify(paginationObj); - - const response = await api.get(`/api/realestate/table/${tableModel}`, { params }); - const data: TableDataResponse = response.data; - - setTableData(data.items || []); - setPagination(data.pagination); - } catch (err: any) { - const errorMessage = - err.response?.data?.detail || err.message || `Fehler beim Laden der Tabelle ${tableModel}`; - setTableDataError(errorMessage); - } finally { - setIsLoadingTableData(false); - } - }, [tableModel]); - - // Load table data on mount - useEffect(() => { - loadTableData(); - }, [loadTableData]); - - /** - * Refetch function compatible with GenericDataHook interface - */ - const refetch = useCallback(async (params?: { - page?: number; - pageSize?: number; - sort?: Array<{field: string; direction: 'asc' | 'desc'}>; - filters?: any; - search?: string; - }) => { - await loadTableData( - params?.page, - params?.pageSize, - params?.sort, - params?.filters, - params?.search - ); - }, [loadTableData]); - - return { - data: tableData, - loading: isLoadingTableData, - error: tableDataError, - refetch, - pagination: pagination || null, - columns: undefined // Columns can be loaded from attributes API if needed - }; - }; -} - -// Attribute definition interface -interface AttributeDefinition { - name: string; - label: string; - type: AttributeType; - sortable?: boolean; - filterable?: boolean; - searchable?: boolean; - width?: number; - minWidth?: number; - maxWidth?: number; - filterOptions?: string[]; - editable?: boolean; - visible?: boolean; -} - -// Helper function to convert attribute definitions to column config -const attributesToColumns = (attributes: AttributeDefinition[], hiddenColumns: string[] = []) => { - return attributes - .filter(attr => !hiddenColumns.includes(attr.name) && !hiddenColumns.includes(attr.label)) - .map(attr => { - // Use attributeTypeMapper to check if this is a date/timestamp field - disable filtering for these - const isDateField = isDateTimeType(attr.type); - - return { - key: attr.name, - label: attr.label || attr.name, - type: attr.type || 'string', - width: attr.width || 200, - minWidth: attr.minWidth || 100, - maxWidth: attr.maxWidth || 400, - sortable: attr.sortable !== false, - // Disable filtering for date/timestamp fields - filterable: isDateField ? false : (attr.filterable !== false), - searchable: attr.searchable !== false, - filterOptions: attr.filterOptions - }; - }); -}; - -/** - * Hook factory for Projekte table with edit/delete support - */ -export function createProjectsTableHook(): () => GenericDataHook { - return () => { - const [tableData, setTableData] = useState([]); - const [isLoadingTableData, setIsLoadingTableData] = useState(false); - const [tableDataError, setTableDataError] = useState(null); - const [pagination, setPagination] = useState(null); - const [attributes, setAttributes] = useState([]); - const [permissions, setPermissions] = useState(null); - const [editingProjects, setEditingProjects] = useState>(new Set()); - const [deletingProjects, setDeletingProjects] = useState>(new Set()); - const { request } = useApiRequest(); - const { checkPermission } = usePermissions(); - - // Columns to hide in Projekte table - const hiddenColumns = ['mandateId', 'Mandat Id', 'perimeter', 'Perimeter', 'dokumente', 'Dokumente', 'kontextInformationen', 'Kontext Informationen']; - - // Fetch attributes from backend - const fetchAttributesData = useCallback(async () => { - try { - const attrs = await fetchAttributes(request, 'Projekt'); - setAttributes(attrs); - return attrs; - } catch (error: any) { - console.error('Error fetching Projekt attributes:', error); - setAttributes([]); - return []; - } - }, [request]); - - // Fetch permissions from backend - const fetchPermissionsData = useCallback(async () => { - try { - const perms = await checkPermission('DATA', 'Projekt'); - setPermissions(perms); - return perms; - } catch (error: any) { - console.error('Error fetching permissions:', error); - const defaultPerms: UserPermissions = { - view: false, - read: 'n', - create: 'n', - update: 'n', - delete: 'n', - }; - setPermissions(defaultPerms); - return defaultPerms; - } - }, [checkPermission]); - - // Generate columns from attributes - const generatedColumns = attributes && attributes.length > 0 - ? attributesToColumns(attributes, hiddenColumns) - : undefined; - - /** - * Load data for the specific table - */ - const loadTableData = useCallback(async ( - page?: number, - pageSize?: number, - sort?: Array<{ field: string; direction: 'asc' | 'desc' }>, - filters?: any, - search?: string - ) => { - setIsLoadingTableData(true); - setTableDataError(null); - - try { - const params: any = {}; - - // Build pagination object - const paginationObj: any = { - page: page || 1, - pageSize: pageSize || 10, - sort: sort || [] - }; - - if (filters) { - paginationObj.filters = filters; - } - - if (search) { - paginationObj.search = search; - } - - params.pagination = JSON.stringify(paginationObj); - - const response = await api.get(`/api/realestate/table/Projekt`, { params }); - const data: TableDataResponse = response.data; - - setTableData(data.items || []); - setPagination(data.pagination); - } catch (err: any) { - const errorMessage = - err.response?.data?.detail || err.message || `Fehler beim Laden der Tabelle Projekt`; - setTableDataError(errorMessage); - } finally { - setIsLoadingTableData(false); - } - }, []); - - // Fetch a single project by ID - const fetchProjectById = useCallback(async (id: string): Promise => { - try { - // Load all projects and find the one with matching ID - // Note: If backend supports GET /api/realestate/table/Projekt/{id}, use that instead - const response = await api.get(`/api/realestate/table/Projekt`); - const data: TableDataResponse = response.data; - const project = (data.items || []).find((item: any) => item.id === id || item.id?.toString() === id); - return project || null; - } catch (err: any) { - console.error('Error fetching project by ID:', err); - return null; - } - }, []); - - // Update project - const handleProjectUpdate = useCallback(async (id: string, updateData: any): Promise<{ success: boolean }> => { - try { - setEditingProjects(prev => new Set(prev).add(id)); - - // Use command API for update - const response = await api.post('/api/realestate/command', { - userInput: `UPDATE Projekt ${id} with ${JSON.stringify(updateData)}` - }); - - const data: CommandResponse = response.data; - - if (data.success) { - // Refetch table data - await loadTableData(); - return { success: true }; - } else { - return { success: false }; - } - } catch (err: any) { - console.error('Error updating project:', err); - return { success: false }; - } finally { - setEditingProjects(prev => { - const next = new Set(prev); - next.delete(id); - return next; - }); - } - }, [loadTableData]); - - // Delete project - const handleProjectDelete = useCallback(async (id: string): Promise => { - try { - setDeletingProjects(prev => new Set(prev).add(id)); - - // Use command API for delete - const response = await api.post('/api/realestate/command', { - userInput: `DELETE Projekt ${id}` - }); - - const data: CommandResponse = response.data; - - if (data.success) { - // Refetch table data - await loadTableData(); - return true; - } else { - return false; - } - } catch (err: any) { - console.error('Error deleting project:', err); - return false; - } finally { - setDeletingProjects(prev => { - const next = new Set(prev); - next.delete(id); - return next; - }); - } - }, [loadTableData]); - - // Create project - const handleProjectCreate = useCallback(async (projectData: any): Promise<{ success: boolean; data?: any; error?: string }> => { - try { - // The projectData now contains: - // - label: string - // - parzellen: Array<{ ...all parcel data including userAddress, geometry, map_view, adjacent_parcels, etc. }> - // - mandateId is NOT included (set by backend) - - // Backward compatibility: if parzelle (singular) exists, convert to parzellen array - if (projectData.parzelle && !projectData.parzellen) { - projectData.parzellen = [projectData.parzelle]; - delete projectData.parzelle; - } - - // Clean and flatten parzellen data: Extract parcel data from ParcelSearchResponse structure - // Each parcel should use its own address data from Swiss Topo API - if (projectData.parzellen && Array.isArray(projectData.parzellen)) { - projectData.parzellen = projectData.parzellen.map((parzelleItem: any) => { - // Handle both structures: - // 1. ParcelSearchResponse structure: { parcel: {...}, map_view: {...}, adjacent_parcels: [...] } - // 2. Already flattened structure: { id, address, plz, ... } - let parcelData: any; - - if (parzelleItem.parcel) { - // ParcelSearchResponse structure - extract parcel data and merge with map_view/adjacent_parcels - parcelData = { - ...parzelleItem.parcel, // All parcel fields (id, address, plz, perimeter, etc.) - // Preserve map_view and adjacent_parcels if needed - map_view: parzelleItem.map_view, - adjacent_parcels: parzelleItem.adjacent_parcels - }; - } else { - // Already flattened - use as-is - parcelData = { ...parzelleItem }; - } - - // Remove userAddress to ensure Swiss Topo API data is used - delete parcelData.userAddress; - - // Ensure address and plz from Swiss Topo are preserved - // These come from the parcel search API response for THIS specific parcel - return parcelData; - }); - } - - // Send the complete project data structure to the backend - const response = await api.post('/api/realestate/table/Projekt', projectData); - - // Refetch table data after successful creation - await loadTableData(); - return { success: true, data: response.data }; - } catch (err: any) { - console.error('Error creating project:', err); - const errorMessage = err.response?.data?.detail || err.message || 'Fehler beim Erstellen des Projekts'; - return { success: false, error: errorMessage }; - } - }, [loadTableData]); - - // Handle single project deletion for FormGenerator - const handleDeleteSingle = useCallback(async (project: any) => { - const success = await handleProjectDelete(project.id); - - if (success) { - await loadTableData(); - } - }, [handleProjectDelete, loadTableData]); - - // Handle multiple project deletion for FormGenerator - const handleDeleteMultiple = useCallback(async (selectedProjects: any[]) => { - const projectIds = selectedProjects.map(project => project.id); - - // Delete all projects sequentially - const results = await Promise.all( - projectIds.map(id => handleProjectDelete(id)) - ); - - const allSuccessful = results.every(result => result); - - if (allSuccessful) { - await loadTableData(); - } - }, [handleProjectDelete, loadTableData]); - - // Optimistic update - const updateOptimistically = useCallback((id: string, updateData: any) => { - setTableData(prev => prev.map(item => { - if (item.id === id || item.id?.toString() === id) { - return { ...item, ...updateData }; - } - return item; - })); - }, []); - - // Optimistic remove - const removeOptimistically = useCallback((id: string) => { - setTableData(prev => prev.filter(item => item.id !== id && item.id?.toString() !== id)); - }, []); - - // Generate edit fields from attributes for create button - const generateEditFieldsFromAttributes = useCallback((): Array<{ - key: string; - label: string; - type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly'; - editable?: boolean; - required?: boolean; - validator?: (value: any) => string | null; - minRows?: number; - maxRows?: number; - options?: Array<{ value: string | number; label: string }>; - optionsReference?: string; - }> => { - if (!attributes || attributes.length === 0) { - return []; - } - - const editableFields = attributes - .filter(attr => { - // Show all fields from backend - filter out ID fields and mandateId for create forms - // mandateId is set by backend, not editable by user - const nonEditableFields = ['id', 'mandateId']; // Filter out ID fields and mandateId for create forms - return !nonEditableFields.includes(attr.name); - }) - .map(attr => { - // Map backend attribute type to form field type using attributeTypeMapper utilities - let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' = 'string'; - let options: Array<{ value: string | number; label: string }> | undefined = undefined; - let optionsReference: string | undefined = undefined; - - const attrType = attr.type as AttributeType; - - // Use attributeTypeMapper utilities to determine field type - if (isCheckboxType(attrType)) { - fieldType = 'boolean'; - } else if (attrType === 'email') { - fieldType = 'email'; - } else if (isDateTimeType(attrType)) { - fieldType = 'date'; - } else if (isSelectType(attrType)) { - fieldType = 'enum'; - const attrOptions = (attr as any).options; - if (Array.isArray(attrOptions)) { - options = attrOptions.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); - } else if (typeof attrOptions === 'string') { - optionsReference = attrOptions; - } - } else if (isMultiselectType(attrType)) { - fieldType = 'multiselect'; - const attrOptions = (attr as any).options; - if (Array.isArray(attrOptions)) { - options = attrOptions.map((opt: any) => { - const labelValue = typeof opt.label === 'string' - ? opt.label - : opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); - return { - value: opt.value, - label: labelValue - }; - }); - } else if (typeof attrOptions === 'string') { - optionsReference = attrOptions; - } - } else if (isTextareaType(attrType)) { - fieldType = 'textarea'; - } else if (attrType === 'readonly') { - fieldType = 'readonly'; - } else { - fieldType = 'string'; - } - - return { - key: attr.name, - label: attr.label || attr.name, - type: fieldType, - required: (attr as any).required || false, - placeholder: (attr as any).placeholder, - editable: true, // All fields from backend should be editable in create form - minRows: isTextareaType(attrType) ? 4 : undefined, - maxRows: isTextareaType(attrType) ? 8 : undefined, - options: options, - optionsReference: optionsReference - }; - }); - - return editableFields; - }, [attributes, hiddenColumns]); - - // Ensure attributes are loaded - const ensureAttributesLoaded = useCallback(async () => { - if (attributes.length === 0) { - await fetchAttributesData(); - } - }, [attributes.length, fetchAttributesData]); - - // Load attributes and permissions first, then table data - useEffect(() => { - const initializeData = async () => { - // Load attributes first to ensure columns are available - await fetchAttributesData(); - await fetchPermissionsData(); - // Then load table data - await loadTableData(); - }; - initializeData(); - }, [fetchAttributesData, fetchPermissionsData, loadTableData]); - - /** - * Refetch function compatible with GenericDataHook interface - */ - const refetch = useCallback(async (params?: { - page?: number; - pageSize?: number; - sort?: Array<{field: string; direction: 'asc' | 'desc'}>; - filters?: any; - search?: string; - }) => { - await loadTableData( - params?.page, - params?.pageSize, - params?.sort, - params?.filters, - params?.search - ); - }, [loadTableData]); - - return { - data: tableData, - loading: isLoadingTableData, - error: tableDataError, - refetch, - pagination: pagination || null, - columns: generatedColumns, - // Operations - handleProjectCreate, - handleProjectUpdate, - handleDelete: handleProjectDelete, - handleDeleteMultiple, - // FormGenerator specific handlers - onDelete: handleDeleteSingle, - onDeleteMultiple: handleDeleteMultiple, - // Loading states - editingProjects, - deletingProjects, - // Optimistic updates - updateOptimistically, - removeOptimistically, - // Attributes and permissions - attributes, - permissions, - // Functions for EditActionButton - fetchProjectById, - ensureAttributesLoaded, - // Functions for CreateButton - generateEditFieldsFromAttributes, - // Entity type - entityType: 'Projekt' - }; - }; -} - -/** - * Hook factory for Parzellen table with edit/delete/view support - */ -export function createParzellenTableHook(): () => GenericDataHook { - return () => { - const [tableData, setTableData] = useState([]); - const [isLoadingTableData, setIsLoadingTableData] = useState(false); - const [tableDataError, setTableDataError] = useState(null); - const [pagination, setPagination] = useState(null); - const [attributes, setAttributes] = useState([]); - const [permissions, setPermissions] = useState(null); - const [editingParzellen, setEditingParzellen] = useState>(new Set()); - const [deletingParzellen, setDeletingParzellen] = useState>(new Set()); - const [viewingParzellen, setViewingParzellen] = useState>(new Set()); - const { request } = useApiRequest(); - const { checkPermission } = usePermissions(); - - // Columns to hide in Parzellen table - const hiddenColumns = [ - 'mandateId', 'Mandate ID', 'Mandat Id', - 'aliasTags', 'Alias Tags', - 'perimeter', 'Perimeter', - 'baulinie', 'Baulinie', - 'bauzone', 'Bauzone', - 'az', 'Az', 'AZ', - 'bz', 'Bz', 'BZ', - 'vollgeschossZahl', 'Vollgeschoss Zahl', - 'anrechenbarDachgeschoss', 'Anrechenbar Dachgeschoss', - 'anrechenbarUntergeschoss', 'Anrechendbar Untergeschoss', - 'gebaudehoheMax', 'Gebäudehöhe Max', - 'regelnGrenzabstand', 'Regeln Grenzabstand', - 'regelnMehrhoehenzuschlag', 'Regln Mehrhoehenzuschlag', - 'parzelleBebaut', 'Parzelle Bebaut', - 'parzelleErschlossen', 'Parzelle Erschlossen', - 'parzelleHanglage', 'Parzelle Hanglage', - 'laermschutzzone', 'Lärmschutzzone', - 'gebaeudehoeheMax', 'Gebäudehöhe Max', - 'regelnMehrlaengenzuschlag', 'Regeln Mehrlängenzuschlag', - 'hochwasserschutzzone', 'Hochwasserschutzzone', - 'grundwasserschutzzone', 'Grundwasserschutzzone', - 'dokumente', 'Dokumente', - 'kontextInformationen', 'Kontext Informationen' - ]; - - // Fetch attributes from backend - const fetchAttributesData = useCallback(async () => { - try { - const attrs = await fetchAttributes(request, 'Parzelle'); - setAttributes(attrs); - return attrs; - } catch (error: any) { - console.error('Error fetching Parzelle attributes:', error); - setAttributes([]); - return []; - } - }, [request]); - - // Fetch permissions from backend - const fetchPermissionsData = useCallback(async () => { - try { - const perms = await checkPermission('DATA', 'Parzelle'); - setPermissions(perms); - return perms; - } catch (error: any) { - console.error('Error fetching permissions:', error); - const defaultPerms: UserPermissions = { - view: false, - read: 'n', - create: 'n', - update: 'n', - delete: 'n', - }; - setPermissions(defaultPerms); - return defaultPerms; - } - }, [checkPermission]); - - // Generate columns from attributes - const generatedColumns = attributes && attributes.length > 0 - ? attributesToColumns(attributes, hiddenColumns) - : undefined; - - /** - * Load data for the specific table - */ - const loadTableData = useCallback(async ( - page?: number, - pageSize?: number, - sort?: Array<{ field: string; direction: 'asc' | 'desc' }>, - filters?: any, - search?: string - ) => { - setIsLoadingTableData(true); - setTableDataError(null); - - try { - const params: any = {}; - - // Build pagination object - const paginationObj: any = { - page: page || 1, - pageSize: pageSize || 10, - sort: sort || [] - }; - - if (filters) { - paginationObj.filters = filters; - } - - if (search) { - paginationObj.search = search; - } - - params.pagination = JSON.stringify(paginationObj); - - const response = await api.get(`/api/realestate/table/Parzelle`, { params }); - const data: TableDataResponse = response.data; - - setTableData(data.items || []); - setPagination(data.pagination); - } catch (err: any) { - const errorMessage = - err.response?.data?.detail || err.message || `Fehler beim Laden der Tabelle Parzelle`; - setTableDataError(errorMessage); - } finally { - setIsLoadingTableData(false); - } - }, []); - - // Fetch a single parzelle by ID - const fetchParzelleById = useCallback(async (id: string): Promise => { - try { - // Load all parzellen and find the one with matching ID - const response = await api.get(`/api/realestate/table/Parzelle`); - const data: TableDataResponse = response.data; - const parzelle = (data.items || []).find((item: any) => item.id === id || item.id?.toString() === id); - return parzelle || null; - } catch (err: any) { - console.error('Error fetching parzelle by ID:', err); - return null; - } - }, []); - - // Update parzelle - const handleParzelleUpdate = useCallback(async (id: string, updateData: any): Promise<{ success: boolean }> => { - try { - setEditingParzellen(prev => new Set(prev).add(id)); - - // Use command API for update - const response = await api.post('/api/realestate/command', { - userInput: `UPDATE Parzelle ${id} with ${JSON.stringify(updateData)}` - }); - - const data: CommandResponse = response.data; - - if (data.success) { - // Refetch table data - await loadTableData(); - return { success: true }; - } else { - return { success: false }; - } - } catch (err: any) { - console.error('Error updating parzelle:', err); - return { success: false }; - } finally { - setEditingParzellen(prev => { - const next = new Set(prev); - next.delete(id); - return next; - }); - } - }, [loadTableData]); - - // Delete parzelle - const handleParzelleDelete = useCallback(async (id: string): Promise => { - try { - setDeletingParzellen(prev => new Set(prev).add(id)); - - // Use command API for delete - const response = await api.post('/api/realestate/command', { - userInput: `DELETE Parzelle ${id}` - }); - - const data: CommandResponse = response.data; - - if (data.success) { - // Refetch table data - await loadTableData(); - return true; - } else { - return false; - } - } catch (err: any) { - console.error('Error deleting parzelle:', err); - return false; - } finally { - setDeletingParzellen(prev => { - const next = new Set(prev); - next.delete(id); - return next; - }); - } - }, [loadTableData]); - - // View parzelle (for preview/details) - const handleParzelleView = useCallback(async (parzelle: any): Promise => { - const id = parzelle.id || parzelle.id?.toString(); - if (id) { - setViewingParzellen(prev => new Set(prev).add(id)); - // The actual viewing is handled by the ViewActionButton component - // This is just for tracking loading state - } - }, []); - - // Optimistic update - const updateOptimistically = useCallback((id: string, updateData: any) => { - setTableData(prev => prev.map(item => { - if (item.id === id || item.id?.toString() === id) { - return { ...item, ...updateData }; - } - return item; - })); - }, []); - - // Optimistic remove - const removeOptimistically = useCallback((id: string) => { - setTableData(prev => prev.filter(item => item.id !== id && item.id?.toString() !== id)); - }, []); - - // Ensure attributes are loaded - const ensureAttributesLoaded = useCallback(async () => { - if (attributes.length === 0) { - await fetchAttributesData(); - } - }, [attributes.length, fetchAttributesData]); - - // Load table data and attributes on mount - useEffect(() => { - const initializeData = async () => { - // Load attributes first to ensure columns are available - await fetchAttributesData(); - await fetchPermissionsData(); - // Then load table data - await loadTableData(); - }; - initializeData(); - }, [fetchAttributesData, fetchPermissionsData, loadTableData]); - - /** - * Refetch function compatible with GenericDataHook interface - */ - const refetch = useCallback(async (params?: { - page?: number; - pageSize?: number; - sort?: Array<{field: string; direction: 'asc' | 'desc'}>; - filters?: any; - search?: string; - }) => { - await loadTableData( - params?.page, - params?.pageSize, - params?.sort, - params?.filters, - params?.search - ); - }, [loadTableData]); - - // Handle single parzelle deletion for FormGenerator - const handleDeleteSingle = useCallback(async (parzelle: any) => { - const success = await handleParzelleDelete(parzelle.id); - - if (success) { - await loadTableData(); - } - }, [handleParzelleDelete, loadTableData]); - - // Handle multiple parzelle deletion for FormGenerator - const handleDeleteMultiple = useCallback(async (selectedParzellen: any[]) => { - const parzelleIds = selectedParzellen.map(parzelle => parzelle.id); - - // Delete all parzellen sequentially - const results = await Promise.all( - parzelleIds.map(id => handleParzelleDelete(id)) - ); - - const allSuccessful = results.every(result => result); - - if (allSuccessful) { - await loadTableData(); - } - }, [handleParzelleDelete, loadTableData]); - - return { - data: tableData, - loading: isLoadingTableData, - error: tableDataError, - refetch, - pagination: pagination || null, - columns: generatedColumns, - // Operations - handleParzelleUpdate, - handleDelete: handleParzelleDelete, - handleDeleteMultiple, - handleParzelleView, - // FormGenerator specific handlers - onDelete: handleDeleteSingle, - onDeleteMultiple: handleDeleteMultiple, - // Loading states - editingParzellen, - deletingParzellen, - viewingParzellen, - // Optimistic updates - updateOptimistically, - removeOptimistically, - // Attributes and permissions - attributes, - permissions, - // Functions for EditActionButton - fetchParzelleById, - ensureAttributesLoaded, - // Entity type - entityType: 'Parzelle' - }; - }; -} - diff --git a/src/hooks/useRoles.ts b/src/hooks/useRoles.ts deleted file mode 100644 index c099ab9..0000000 --- a/src/hooks/useRoles.ts +++ /dev/null @@ -1,237 +0,0 @@ -/** - * useRoles Hook - * - * Hook für die Verwaltung von globalen RBAC-Rollen im Admin-Bereich. - * Folgt dem gleichen Pattern wie useOrgUsers. - */ - -import { useState, useEffect, useCallback } from 'react'; -import { useApiRequest } from './useApi'; -import api from '../api'; -import { usePermissions, type UserPermissions } from './usePermissions'; -import { - fetchRoles as fetchRolesApi, - fetchRoleById as fetchRoleByIdApi, - createRole as createRoleApi, - updateRole as updateRoleApi, - deleteRole as deleteRoleApi, - type Role, - type RoleUpdateData, - type PaginationParams -} from '../api/roleApi'; - -// Re-export types -export type { Role, RoleUpdateData, PaginationParams }; - -export interface AttributeDefinition { - name: string; - type: string; - label: string; - description?: string; - required?: boolean; - default?: any; - options?: Array<{ value: string | number; label: string | { [key: string]: string } }> | string; - sortable?: boolean; - filterable?: boolean; - searchable?: boolean; - width?: number; - minWidth?: number; - maxWidth?: number; - readonly?: boolean; - editable?: boolean; -} - -/** - * Hook for managing RBAC roles in admin panel - */ -export function useAdminRoles() { - const [roles, setRoles] = useState([]); - const [attributes, setAttributes] = useState([]); - const [permissions, setPermissions] = useState(null); - const [pagination, setPagination] = useState<{ - currentPage: number; - pageSize: number; - totalItems: number; - totalPages: number; - } | null>(null); - const { request, isLoading: loading, error } = useApiRequest(); - const { checkPermission } = usePermissions(); - - // Fetch attributes from backend - const fetchAttributes = useCallback(async () => { - try { - const response = await api.get('/api/attributes/Role'); - - let attrs: AttributeDefinition[] = []; - if (response.data?.attributes && Array.isArray(response.data.attributes)) { - attrs = response.data.attributes; - } else if (Array.isArray(response.data)) { - attrs = response.data; - } else if (response.data && typeof response.data === 'object') { - const keys = Object.keys(response.data); - for (const key of keys) { - if (Array.isArray(response.data[key])) { - attrs = response.data[key]; - break; - } - } - } - - setAttributes(attrs); - return attrs; - } catch (error: any) { - if (error.response?.status === 429) { - console.warn('Rate limit exceeded while fetching role attributes.'); - } else if (error.response?.status !== 401) { - console.error('Error fetching role attributes:', error); - } - setAttributes([]); - return []; - } - }, []); - - // Fetch permissions - const fetchPermissions = useCallback(async () => { - try { - const perms = await checkPermission('DATA', 'Role'); - setPermissions(perms); - return perms; - } catch (error: any) { - console.error('Error fetching role permissions:', error); - const defaultPerms: UserPermissions = { - view: false, - read: 'n', - create: 'n', - update: 'n', - delete: 'n', - }; - setPermissions(defaultPerms); - return defaultPerms; - } - }, [checkPermission]); - - // Fetch roles - const fetchRoles = useCallback(async (params?: PaginationParams) => { - try { - const data = await fetchRolesApi(request, params); - - if (data && typeof data === 'object' && 'items' in data) { - const items = Array.isArray(data.items) ? data.items : []; - setRoles(items); - if (data.pagination) { - setPagination(data.pagination); - } - } else { - const items = Array.isArray(data) ? data : []; - setRoles(items); - setPagination(null); - } - } catch (error: any) { - setRoles([]); - setPagination(null); - } - }, [request]); - - // Optimistic updates - const removeOptimistically = (roleId: string) => { - setRoles(prev => prev.filter(r => r.id !== roleId)); - }; - - const updateOptimistically = (roleId: string, updateData: Partial) => { - setRoles(prev => - prev.map(r => r.id === roleId ? { ...r, ...updateData } : r) - ); - }; - - // Fetch single role - const fetchRoleById = useCallback(async (roleId: string): Promise => { - return await fetchRoleByIdApi(request, roleId); - }, [request]); - - // Generate columns from attributes (including fkSource/fkDisplayField for FK resolution) - const columns = attributes.map(attr => ({ - key: attr.name, - label: attr.label || attr.name, - type: attr.type as any, - sortable: attr.sortable !== false, - filterable: attr.filterable !== false, - searchable: attr.searchable !== false, - width: attr.width || 150, - minWidth: attr.minWidth || 100, - maxWidth: attr.maxWidth || 400, - fkSource: (attr as any).fkSource, // API endpoint for FK data - fkDisplayField: (attr as any).fkDisplayField, // Which field of FK target to display - })); - - // Create role - const handleCreate = useCallback(async (roleData: Partial): Promise => { - try { - await createRoleApi(request, roleData); - await fetchRoles(); - return true; - } catch (error: any) { - console.error('Error creating role:', error); - return false; - } - }, [request, fetchRoles]); - - // Update role - const handleUpdate = useCallback(async (roleId: string, updateData: RoleUpdateData): Promise => { - try { - updateOptimistically(roleId, updateData); - await updateRoleApi(request, roleId, updateData); - return true; - } catch (error: any) { - console.error('Error updating role:', error); - await fetchRoles(); - return false; - } - }, [request, fetchRoles]); - - // Delete role - const handleDelete = useCallback(async (roleId: string): Promise => { - try { - removeOptimistically(roleId); - await deleteRoleApi(request, roleId); - return true; - } catch (error: any) { - console.error('Error deleting role:', error); - await fetchRoles(); - return false; - } - }, [request, fetchRoles]); - - // Inline update - const handleInlineUpdate = useCallback(async ( - roleId: string, - updateData: Partial - ): Promise => { - await handleUpdate(roleId, updateData); - }, [handleUpdate]); - - // Load data on mount - useEffect(() => { - fetchAttributes(); - fetchPermissions(); - fetchRoles(); - }, []); - - return { - roles, - attributes, - columns, - permissions, - pagination, - loading, - error, - refetch: fetchRoles, - fetchRoleById, - handleCreate, - handleUpdate, - handleDelete, - handleInlineUpdate, - updateOptimistically, - }; -} - -export default useAdminRoles; diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts deleted file mode 100644 index 8037ebf..0000000 --- a/src/hooks/useSettings.ts +++ /dev/null @@ -1,389 +0,0 @@ -import { useState, useEffect, useCallback, useRef } from 'react'; -import { useUser, useCurrentUser } from './useUsers'; -import { useApiRequest } from './useApi'; -import { GenericDataHook } from '../core/PageManager/pageInterface'; -import type { SettingsFieldConfig } from '../core/PageManager/pageInterface'; - -// Interface for unified settings data -export interface SettingsData { - // User data (from API) - username?: string; - fullName?: string; - email?: string; - language?: string; - privilege?: string; - enabled?: boolean; - authenticationAuthority?: string; - // Phone name (localStorage) - phoneName?: string; - // Theme (localStorage) - theme?: string; - // Speech data (localStorage) - speechData?: any; - // Nested speech fields - mandate_general?: { - company_name?: string; - industry?: string; - contact_info?: { - email?: string; - phone?: string; - street?: string; - postal_code?: string; - city?: string; - country?: string; - }; - business_hours?: string; - timezone?: string; - }; - setup_contacts?: boolean; -} - -// Create settings hook factory -export function createSettingsHook(): () => GenericDataHook { - return function useSettings() { - const { user: currentUser } = useCurrentUser(); - const { getUser, updateUser } = useUser(); - const { request } = useApiRequest(); - - const [settingsData, setSettingsData] = useState({}); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [settingsFields, setSettingsFields] = useState>({}); - const [settingsLoading, setSettingsLoading] = useState>({}); - const [settingsErrors, setSettingsErrors] = useState>({}); - - // Track if we've loaded data initially to prevent infinite loops - const hasLoadedRef = useRef(false); - const currentUserIdRef = useRef(currentUser?.id); - - // Load phone name from localStorage - const _loadPhoneName = useCallback((): string => { - try { - return localStorage.getItem('userPhoneName') || ''; - } catch (error) { - console.error('Failed to load phone name from localStorage:', error); - return ''; - } - }, []); - void _loadPhoneName; // Intentionally unused, reserved for future use - - // Load theme from localStorage - const _loadTheme = useCallback((): string => { - try { - const savedTheme = localStorage.getItem('theme'); - if (savedTheme) { - return savedTheme; - } - // Default to system preference - return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; - } catch (error) { - console.error('Failed to load theme from localStorage:', error); - return 'light'; - } - }, []); - void _loadTheme; // Intentionally unused, reserved for future use - - // Load speech data from localStorage - const _loadSpeechData = useCallback((): any | null => { - try { - const savedData = localStorage.getItem('speechSignUpData'); - const timestamp = localStorage.getItem('speechSignUpTimestamp'); - - if (savedData && timestamp) { - const parsedData = JSON.parse(savedData); - const savedTime = parseInt(timestamp); - const now = Date.now(); - const twentyFourHours = 24 * 60 * 60 * 1000; - - // Check if data is still valid (within 24 hours) - if (now - savedTime < twentyFourHours) { - return parsedData; - } else { - // Data expired, clear it - localStorage.removeItem('speechSignUpData'); - localStorage.removeItem('speechSignUpTimestamp'); - return null; - } - } - return null; - } catch (error) { - console.error('Error loading speech data:', error); - return null; - } - }, []); - void _loadSpeechData; // Intentionally unused, reserved for future use - - // Fetch user data from API - const _fetchUserData = useCallback(async () => { - if (!currentUser?.id) return null; - - try { - const userData = await getUser(currentUser.id); - return userData; - } catch (error) { - console.error('Failed to fetch user data:', error); - throw error; - } - }, [currentUser?.id, getUser]); - void _fetchUserData; // Intentionally unused, reserved for future use - - // Fetch field definitions from backend - const _fetchFieldsForSection = useCallback(async (sectionId: string): Promise => { - try { - setSettingsLoading(prev => ({ ...prev, [sectionId]: true })); - setSettingsErrors(prev => ({ ...prev, [sectionId]: null })); - - // TODO: Replace with actual backend endpoint - // For now, return empty array - fields will come from backend later - const response = await request({ - url: `/api/settings/fields?section=${sectionId}`, - method: 'get' - }); - - const fields = response?.fields || []; - setSettingsFields(prev => ({ ...prev, [sectionId]: fields })); - return fields; - } catch (error: any) { - const errorMessage = error.message || `Failed to load fields for ${sectionId}`; - console.error(`Error fetching fields for section ${sectionId}:`, error); - setSettingsErrors(prev => ({ ...prev, [sectionId]: errorMessage })); - return []; - } finally { - setSettingsLoading(prev => ({ ...prev, [sectionId]: false })); - } - }, [request]); - void _fetchFieldsForSection; // Intentionally unused, reserved for future use - - // Load all settings data - const loadSettingsData = useCallback(async () => { - // Prevent multiple simultaneous loads - if (loading && hasLoadedRef.current) return; - - try { - setLoading(true); - setError(null); - - // Load from different sources - call functions directly to avoid dependency issues - const userData = currentUser?.id ? await getUser(currentUser.id) : null; - const phoneName = localStorage.getItem('userPhoneName') || ''; - const savedTheme = localStorage.getItem('theme'); - const theme = savedTheme || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); - - // Load speech data - let speechData: any | null = null; - try { - const savedData = localStorage.getItem('speechSignUpData'); - const timestamp = localStorage.getItem('speechSignUpTimestamp'); - if (savedData && timestamp) { - const parsedData = JSON.parse(savedData); - const savedTime = parseInt(timestamp); - const now = Date.now(); - const twentyFourHours = 24 * 60 * 60 * 1000; - if (now - savedTime < twentyFourHours) { - speechData = parsedData; - } else { - localStorage.removeItem('speechSignUpData'); - localStorage.removeItem('speechSignUpTimestamp'); - } - } - } catch (error) { - console.error('Error loading speech data:', error); - } - - // Merge all data into unified object - const unifiedData: SettingsData = { - ...(userData || {}), - phoneName, - theme, - speechData, - // Flatten speech data if it exists - ...(speechData?.mandate_general && { - mandate_general: speechData.mandate_general - }), - ...(speechData?.setup_contacts !== undefined && { - setup_contacts: speechData.setup_contacts - }) - }; - - setSettingsData(unifiedData); - hasLoadedRef.current = true; - } catch (error: any) { - console.error('Error loading settings data:', error); - setError(error.message || 'Failed to load settings'); - } finally { - setLoading(false); - } - }, [currentUser?.id, getUser]); // Only depend on currentUser?.id and getUser - - // Save section data - const saveSection = useCallback(async (sectionId: string, data: any) => { - try { - setSettingsLoading(prev => ({ ...prev, [sectionId]: true })); - setSettingsErrors(prev => ({ ...prev, [sectionId]: null })); - - if (sectionId === 'user-info') { - // Separate user API data from localStorage data - const { phoneName, id, mandateId, ...userApiData } = data; - - // Build clean update object with only allowed fields (explicitly exclude id and mandateId) - // Allowed fields: username, email, fullName, language, enabled, privilege, authenticationAuthority - const userUpdateData: Record = {}; - const allowedFields = ['username', 'email', 'fullName', 'language', 'enabled', 'privilege', 'authenticationAuthority']; - - allowedFields.forEach(field => { - if (userApiData.hasOwnProperty(field)) { - userUpdateData[field] = userApiData[field]; - } - }); - - // Save user data via API (only allowed fields, no id or mandateId) - if (currentUser?.id && Object.keys(userUpdateData).length > 0) { - const updatedUser = await updateUser(currentUser.id, userUpdateData); - // Update local state with API data - setSettingsData(prev => ({ ...prev, ...updatedUser })); - - // Update localStorage cache - localStorage.setItem('currentUser', JSON.stringify(updatedUser)); - - // Dispatch event to notify other components - window.dispatchEvent(new CustomEvent('userInfoUpdated')); - } - - // Save phone name to localStorage separately - if (phoneName !== undefined) { - if (phoneName?.trim()) { - localStorage.setItem('userPhoneName', phoneName.trim()); - } else { - localStorage.removeItem('userPhoneName'); - } - // Update local state with phone name - setSettingsData(prev => ({ ...prev, phoneName })); - } - } else if (sectionId === 'theme') { - // Save theme to localStorage - localStorage.setItem('theme', data.theme); - // Apply theme immediately - document.documentElement.setAttribute('data-theme', data.theme); - document.documentElement.classList.remove('light-theme', 'dark-theme'); - document.documentElement.classList.add(`${data.theme}-theme`); - // Update local state - setSettingsData(prev => ({ ...prev, theme: data.theme })); - } else if (sectionId === 'speech-settings') { - // Save speech data to localStorage - localStorage.setItem('speechSignUpData', JSON.stringify(data)); - localStorage.setItem('speechSignUpTimestamp', Date.now().toString()); - // Update local state - setSettingsData(prev => ({ - ...prev, - speechData: data, - ...(data.mandate_general && { mandate_general: data.mandate_general }), - ...(data.setup_contacts !== undefined && { setup_contacts: data.setup_contacts }) - })); - // Dispatch event to notify other components - window.dispatchEvent(new CustomEvent('speechSignUpChanged')); - } else if (sectionId === 'phone-name') { - // Save phone name to localStorage - if (data.phoneName?.trim()) { - localStorage.setItem('userPhoneName', data.phoneName.trim()); - } else { - localStorage.removeItem('userPhoneName'); - } - // Update local state - setSettingsData(prev => ({ ...prev, phoneName: data.phoneName })); - } - } catch (error: any) { - const errorMessage = error.message || `Failed to save ${sectionId}`; - console.error(`Error saving section ${sectionId}:`, error); - setSettingsErrors(prev => ({ ...prev, [sectionId]: errorMessage })); - throw error; - } finally { - setSettingsLoading(prev => ({ ...prev, [sectionId]: false })); - } - }, [currentUser?.id, updateUser]); - - // Initial load - only when currentUser?.id changes or on first mount - useEffect(() => { - // Only load if: - // 1. We haven't loaded yet, OR - // 2. The user ID changed - if (!hasLoadedRef.current || currentUserIdRef.current !== currentUser?.id) { - currentUserIdRef.current = currentUser?.id; - - // Load data directly to avoid dependency issues - const loadData = async () => { - if (loading && hasLoadedRef.current) return; - - try { - setLoading(true); - setError(null); - - // Load from different sources - const userData = currentUser?.id ? await getUser(currentUser.id) : null; - const phoneName = localStorage.getItem('userPhoneName') || ''; - const savedTheme = localStorage.getItem('theme'); - const theme = savedTheme || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); - - // Load speech data - let speechData: any | null = null; - try { - const savedData = localStorage.getItem('speechSignUpData'); - const timestamp = localStorage.getItem('speechSignUpTimestamp'); - if (savedData && timestamp) { - const parsedData = JSON.parse(savedData); - const savedTime = parseInt(timestamp); - const now = Date.now(); - const twentyFourHours = 24 * 60 * 60 * 1000; - if (now - savedTime < twentyFourHours) { - speechData = parsedData; - } else { - localStorage.removeItem('speechSignUpData'); - localStorage.removeItem('speechSignUpTimestamp'); - } - } - } catch (error) { - console.error('Error loading speech data:', error); - } - - // Merge all data into unified object - const unifiedData: SettingsData = { - ...(userData || {}), - phoneName, - theme, - speechData, - ...(speechData?.mandate_general && { - mandate_general: speechData.mandate_general - }), - ...(speechData?.setup_contacts !== undefined && { - setup_contacts: speechData.setup_contacts - }) - }; - - setSettingsData(unifiedData); - hasLoadedRef.current = true; - } catch (error: any) { - console.error('Error loading settings data:', error); - setError(error.message || 'Failed to load settings'); - } finally { - setLoading(false); - } - }; - - loadData(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentUser?.id]); // Only depend on currentUser?.id - - return { - data: [], // Not used for settings - loading, - error, - settingsData, - settingsFields, - settingsLoading, - settingsErrors, - saveSection, - refetch: loadSettingsData - } as GenericDataHook; - }; -} - diff --git a/src/hooks/useSidebar.ts b/src/hooks/useSidebar.ts deleted file mode 100644 index 13dde43..0000000 --- a/src/hooks/useSidebar.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { useState, useEffect } from 'react'; -import { useSidebar as useNewSidebar } from '../core/PageManager/SidebarProvider'; -import { SidebarItem } from '../core/PageManager/pageInterface'; -import { useLanguage } from '../providers/language/LanguageContext'; - -// Hook to get sidebar items from page configurations -export const useSidebarFromPageConfigs = (): { items: SidebarItem[]; isLoading: boolean } => { - const { t } = useLanguage(); - const [refreshTrigger, setRefreshTrigger] = useState(0); - - // Use the new sidebar system - const { sidebarItems: newSidebarItems, loading: newLoading, refreshSidebar } = useNewSidebar(); - - // Listen for localStorage changes to refresh sidebar when sign-up status changes - useEffect(() => { - const handleStorageChange = (e: StorageEvent) => { - if (e.key === 'speechSignUpData' || e.key === 'speechSignUpTimestamp') { - setRefreshTrigger(prev => prev + 1); - } - }; - - window.addEventListener('storage', handleStorageChange); - - // Also listen for changes in the same tab - const handleCustomStorageChange = () => { - setRefreshTrigger(prev => prev + 1); - }; - - // Custom event for same-tab changes - window.addEventListener('speechSignUpChanged', handleCustomStorageChange); - - return () => { - window.removeEventListener('storage', handleStorageChange); - window.removeEventListener('speechSignUpChanged', handleCustomStorageChange); - }; - }, []); - - // Refresh sidebar when trigger changes - useEffect(() => { - if (refreshTrigger > 0) { - refreshSidebar(); - } - }, [refreshTrigger, refreshSidebar]); - - // Map the items with translations - const translatedItems = newSidebarItems.map(item => ({ - ...item, - name: getTranslatedName(item.name, t) - })); - - return { items: translatedItems, isLoading: newLoading }; -}; - -// Helper function to get translated names -// This maps the page config names to translation keys -const getTranslatedName = (name: string, t: (key: string) => string): string => { - const translationMap: Record = { - 'Dashboard': t('nav.dashboard'), - 'Dateien': t('nav.files'), - 'Workflows': t('nav.workflows'), - 'Connections': t('nav.connections'), - 'Team Bereich': t('nav.team'), - 'Einstellungen': t('nav.settings'), - 'Test Sharepoint': t('nav.testSharepoint'), - 'Speech': t('nav.speech'), - 'Transkriptverwaltung': t('nav.transcript_management') - }; - - return translationMap[name] || name; -}; - -// Backward-compatible hook that returns just the items array -export const useSidebarItems = (): SidebarItem[] => { - const { items } = useSidebarFromPageConfigs(); - return items; -}; - -export default useSidebarFromPageConfigs; diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx deleted file mode 100644 index 942621e..0000000 --- a/src/pages/Home/Home.tsx +++ /dev/null @@ -1,70 +0,0 @@ - - -import styles from './HomeStyles/Home.module.css' - -import Sidebar from '../../components/Sidebar'; -import { PageManager, SidebarProvider } from '../../core/PageManager'; -import { useCurrentUser } from '../../hooks/useUsers'; - - - -function Home () { - // Ensure user data is loaded and cached in localStorage for privilege checks - const { isLoading: userLoading, error: userError } = useCurrentUser(); - - // Show loading state while user data is being fetched - if (userLoading) { - return ( -
    -
    - Lade Benutzerdaten... -
    -
    - ); - } - - // Show error state if user data failed to load - if (userError) { - return ( -
    -
    - Fehler beim Laden der Benutzerdaten: {userError} -
    -
    - ); - } - - // Loading component - const LoadingComponent = () => ( -
    - Lade Seite... -
    - ); - - // Error component - const ErrorComponent = () => ( -
    - Seite nicht verfügbar oder deaktiviert -
    - ); - - return ( - -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - ); -} - -export default Home; diff --git a/src/pages/Home/HomeStyles/Home.module.css b/src/pages/Home/HomeStyles/Home.module.css deleted file mode 100644 index dd857ff..0000000 --- a/src/pages/Home/HomeStyles/Home.module.css +++ /dev/null @@ -1,65 +0,0 @@ -.homeContainer { - position: relative; - background-color: var(--color-bg); - min-height: 100vh; - max-height: 100vh; - width: 100vw; - font-family: var(--font-family); - z-index: 0; - overflow: hidden; - } - - .homeContainer::before { - content: ""; - position: absolute; - top: 0; left: 0; - width: 100%; height: 100%; - background-size: 8px 8px; - opacity: 0.4; - z-index: -1; - pointer-events: none; - } - - -.body { - display: flex; - max-width: 100vw; - height: 100vh; -} - -.homeSidebar { - flex-shrink: 0; -} - -.homeContent { - height: 100vh; - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; -} - -/* Page Loader Styles */ -.loadingContainer { - display: flex; - justify-content: center; - align-items: center; - height: 100%; - color: var(--color-text); - font-family: var(--font-family); - font-size: 1rem; - opacity: 0.7; -} - -.errorContainer { - display: flex; - justify-content: center; - align-items: center; - height: 100%; - color: var(--color-text); - font-family: var(--font-family); - font-size: 1rem; - opacity: 0.5; - text-align: center; - padding: 20px; -} \ No newline at end of file diff --git a/src/utils/privilegeCheckers.ts b/src/utils/privilegeCheckers.ts deleted file mode 100644 index 0f207e8..0000000 --- a/src/utils/privilegeCheckers.ts +++ /dev/null @@ -1,308 +0,0 @@ -import { PrivilegeChecker } from '../core/PageManager/pageInterface'; -import { getUserDataCache } from './userCache'; -import type { PermissionContext } from '../hooks/usePermissions'; - -/** - * Privilege Checkers - * - * Read-only access to user data for privilege checking. - * Does not manage user data storage - that's handled by authentication hooks. - * - * Now supports both client-side checks (roles, localStorage) and backend RBAC integration. - */ - -// Function to get current user role labels from sessionStorage cache -const getCurrentUserRoleLabels = (): string[] => { - const userData = getUserDataCache(); - return Array.isArray(userData?.roleLabels) ? userData.roleLabels : []; -}; - -// Generic privilege checker for localStorage-based data with expiration -export const createLocalStoragePrivilegeChecker = ( - dataKey: string, - timestampKey: string, - expirationHours: number = 24 -): PrivilegeChecker => { - return (): boolean => { - try { - const savedData = localStorage.getItem(dataKey); - const timestamp = localStorage.getItem(timestampKey); - - if (savedData && timestamp) { - const dataTime = parseInt(timestamp); - const now = Date.now(); - const hoursDiff = (now - dataTime) / (1000 * 60 * 60); - - return hoursDiff < expirationHours; - } - - return false; - } catch (error) { - console.error(`Error checking privilege for ${dataKey}:`, error); - return false; - } - }; -}; - -// Generic privilege checker for user roles/permissions -export const createRolePrivilegeChecker = ( - requiredRoles: string[], - getUserRoles: () => string[] | Promise -): PrivilegeChecker => { - return async (): Promise => { - try { - const userRoles = await getUserRoles(); - const hasRequiredRole = requiredRoles.some(role => userRoles.includes(role)); - - return hasRequiredRole; - } catch (error) { - console.error('Error checking role privilege:', error); - return false; - } - }; -}; - -// Generic privilege checker for feature flags -export const createFeatureFlagChecker = ( - featureFlag: string, - getFeatureFlags: () => Record | Promise> -): PrivilegeChecker => { - return async (): Promise => { - try { - const flags = await getFeatureFlags(); - return flags[featureFlag] === true; - } catch (error) { - console.error(`Error checking feature flag ${featureFlag}:`, error); - return false; - } - }; -}; - -// Generic privilege checker for authentication status -export const createAuthPrivilegeChecker = ( - isAuthenticated: () => boolean | Promise -): PrivilegeChecker => { - return async (): Promise => { - try { - return await isAuthenticated(); - } catch (error) { - console.error('Error checking authentication status:', error); - return false; - } - }; -}; - -// Helper function to create custom privilege checkers -export const createCustomPrivilegeChecker = ( - checkFunction: () => boolean | Promise -): PrivilegeChecker => { - return checkFunction; -}; - -/** - * Create a privilege checker that uses backend RBAC permissions - * This integrates privilegeCheckers with usePermissions for backend-controlled access - * - * @param canViewFunction - The canView function from usePermissions hook - * @param context - Permission context ('UI', 'DATA', or 'RESOURCE') - * @param item - The item/resource path to check permissions for - * @returns A PrivilegeChecker function that checks backend RBAC permissions - */ -export const createRBACPrivilegeChecker = ( - canViewFunction: (context: PermissionContext, item: string) => Promise, - context: PermissionContext, - item: string -): PrivilegeChecker => { - return async (): Promise => { - try { - return await canViewFunction(context, item); - } catch (error) { - console.error(`Error checking RBAC privilege for ${context}:${item}:`, error); - return false; - } - }; -}; - -/** - * Create a privilege checker that combines RBAC with client-side role checks - * First checks backend RBAC, then falls back to client-side role check if RBAC allows - * - * @param canViewFunction - The canView function from usePermissions hook - * @param context - Permission context ('UI', 'DATA', or 'RESOURCE') - * @param item - The item/resource path to check permissions for - * @param requiredRoles - Fallback client-side roles to check if RBAC passes - * @returns A PrivilegeChecker function that checks both RBAC and roles - */ -export const createCombinedPrivilegeChecker = ( - canViewFunction: (context: PermissionContext, item: string) => Promise, - context: PermissionContext, - item: string, - requiredRoles: string[] -): PrivilegeChecker => { - return async (): Promise => { - try { - // First check backend RBAC - const hasRBACAccess = await canViewFunction(context, item); - if (!hasRBACAccess) { - return false; - } - - // If RBAC allows, also check client-side roles as additional validation - const userRoleLabels = getCurrentUserRoleLabels(); - const hasRequiredRole = requiredRoles.some(role => userRoleLabels.includes(role)); - if (hasRequiredRole) { - return true; - } - - // If no role match, still allow if RBAC said yes (backend is source of truth) - return hasRBACAccess; - } catch (error) { - console.error(`Error checking combined privilege for ${context}:${item}:`, error); - return false; - } - }; -}; - -/** - * Helper to create RBAC-based privilege checkers for page data - * These checkers will use backend RBAC permissions via usePermissions - * - * Usage in page data: - * import { createRBACPageChecker } from '@/utils/privilegeCheckers'; - * - * // In PageManager, initialize with canView function: - * const rbacCheckers = createRBACPageCheckers(canView); - * - * // In page data: - * privilegeChecker: rbacCheckers.forPage('administration/workflows') - */ -export const createRBACPageCheckers = ( - canViewFunction: (context: PermissionContext, item: string) => Promise -) => { - return { - /** - * Create a privilege checker for a specific page path - * Checks backend RBAC permissions for UI context - */ - forPage: (pagePath: string): PrivilegeChecker => { - return createRBACPrivilegeChecker(canViewFunction, 'UI', pagePath); - }, - - /** - * Create a privilege checker that combines RBAC with role requirements - * First checks backend RBAC, then validates user role - */ - forPageWithRole: ( - pagePath: string, - requiredRoles: string[] - ): PrivilegeChecker => { - return createCombinedPrivilegeChecker(canViewFunction, 'UI', pagePath, requiredRoles); - }, - - /** - * Create a privilege checker for a data resource - * Checks backend RBAC permissions for DATA context - */ - forData: (resourcePath: string): PrivilegeChecker => { - return createRBACPrivilegeChecker(canViewFunction, 'DATA', resourcePath); - }, - - /** - * Create a privilege checker for a UI resource - * Checks backend RBAC permissions for UI context - */ - forUI: (resourcePath: string): PrivilegeChecker => { - return createRBACPrivilegeChecker(canViewFunction, 'UI', resourcePath); - } - }; -}; - -// Predefined privilege checkers for common use cases -export const privilegeCheckers = { - // Speech signup checker (existing functionality) - speechSignup: createLocalStoragePrivilegeChecker( - 'speechSignUpData', - 'speechSignUpTimestamp', - 24 - ), - - // Admin role checker - for admin and sysadmin users - adminRole: createRolePrivilegeChecker( - ['admin', 'sysadmin'], - () => { - const userRoleLabels = getCurrentUserRoleLabels(); - return Promise.resolve(userRoleLabels); - } - ), - - // Sysadmin role checker - for sysadmin only - sysadminRole: createRolePrivilegeChecker( - ['sysadmin'], - () => { - const userRoleLabels = getCurrentUserRoleLabels(); - return Promise.resolve(userRoleLabels); - } - ), - - // Premium user checker - premiumUser: createLocalStoragePrivilegeChecker( - 'premiumUserData', - 'premiumUserTimestamp', - 24 * 30 // 30 days - ), - - // Feature flag checker - betaFeatures: createFeatureFlagChecker( - 'betaFeatures', - () => { - const flags = JSON.parse(localStorage.getItem('featureFlags') || '{}'); - return Promise.resolve(flags); - } - ), - - // Authentication checker - authenticated: createAuthPrivilegeChecker( - () => { - const token = localStorage.getItem('authToken'); - return Promise.resolve(!!token); - } - ), - - // User role checker - for user, admin, and sysadmin access - userRole: createRolePrivilegeChecker( - ['user', 'admin', 'sysadmin'], - () => { - const userRoleLabels = getCurrentUserRoleLabels(); - return Promise.resolve(userRoleLabels); - } - ), - - // Viewer role checker - for viewer, user, admin, and sysadmin access (all levels) - viewerRole: createRolePrivilegeChecker( - ['viewer', 'user', 'admin', 'sysadmin'], - () => { - const userRoleLabels = getCurrentUserRoleLabels(); - return Promise.resolve(userRoleLabels); - } - ), - - // Subscription checker - for paid features - hasSubscription: createLocalStoragePrivilegeChecker( - 'subscriptionData', - 'subscriptionTimestamp', - 24 * 7 // 7 days - ), - - // Mandate checker - for users who have submitted their mandate - hasMandate: createLocalStoragePrivilegeChecker( - 'mandateData', - 'mandateTimestamp', - 24 * 30 // 30 days - ), - - // Always allow access (for public pages) - alwaysAllow: createCustomPrivilegeChecker(() => true), - - // Never allow access (for disabled features) - neverAllow: createCustomPrivilegeChecker(() => false) -};