Android_Retail/src/Components/Menu/PozoMenu.jsx

472 lines
15 KiB
React
Raw Normal View History

2026-02-06 14:27:37 +05:30
import { useState, useRef, useEffect } from 'react';
import { FaCaretRight } from 'react-icons/fa';
2026-01-27 18:27:29 +05:30
import './PozoMenu.scss';
2026-03-10 15:49:03 +05:30
import { getSession, sessionStore } from '../../Services/Others';
2026-01-27 18:27:29 +05:30
const PozoMenu = ({
items = [],
mode = 'vertical',
style = {},
onClick,
collapse = false,
selectedKey: propSelectedKey,
isBottomMenu = false,
}) => {
2026-01-27 18:27:29 +05:30
const [openKeys, setOpenKeys] = useState([]);
const [selectedKey, setSelectedKey] = useState('');
const [dropdownPositions, setDropdownPositions] = useState({});
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
const BranchId = getSession('BranchId');
2026-01-27 18:27:29 +05:30
const menuRef = useRef();
// Update selected key when prop changes
useEffect(() => {
if (propSelectedKey !== undefined) {
setSelectedKey(propSelectedKey);
}
}, [propSelectedKey]);
// Close dropdown when clicking outside, window resize, or scroll
useEffect(() => {
const handleClickOutside = (event) => {
if (menuRef.current && !menuRef.current.contains(event.target)) {
setOpenKeys([]);
setDropdownPositions({});
}
};
const handleResize = () => {
// Update mobile detection
setIsMobile(window.innerWidth <= 768);
// Recalculate positions on window resize
if (openKeys.length > 0) {
setOpenKeys([]);
setDropdownPositions({});
}
};
const handleScroll = () => {
// 🎯 FIXED: Recalculate positions on scroll to maintain proper positioning
if (openKeys.length > 0) {
// Find all currently open menu elements and recalculate their positions
const newPositions = {};
openKeys.forEach((key) => {
2026-01-27 18:27:29 +05:30
const element = document.querySelector(`[data-menu-key="${key}"]`);
if (element) {
const level = key.split('-').length - 1;
newPositions[key] = calculatePosition(element, level);
}
});
setDropdownPositions((prev) => ({ ...prev, ...newPositions }));
2026-01-27 18:27:29 +05:30
}
};
document.addEventListener('mousedown', handleClickOutside);
window.addEventListener('resize', handleResize);
window.addEventListener('scroll', handleScroll, true); // Capture scroll events
return () => {
document.removeEventListener('mousedown', handleClickOutside);
window.removeEventListener('resize', handleResize);
window.removeEventListener('scroll', handleScroll, true);
};
}, [openKeys]);
// 🎯 FIXED: Smart positioning calculation - exactly like your image behavior
const calculatePosition = (element, level = 0) => {
const rect = element.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const scrollX = window.pageXOffset || document.documentElement.scrollLeft;
const scrollY = window.pageYOffset || document.documentElement.scrollTop;
// Dynamic dropdown dimensions - responsive
const dropdownWidth = isMobile
? Math.min(280, viewportWidth - 20)
: level > 0
? 220
: 250;
2026-01-27 18:27:29 +05:30
const dropdownHeight = Math.min(
isMobile ? viewportHeight * 0.5 : 400,
viewportHeight * 0.6
);
const gap = isMobile ? 2 : 4;
const edgePadding = isMobile ? 10 : 8;
// Calculate available space on both sides
const spaceRight = viewportWidth - rect.right;
const spaceLeft = rect.left;
const spaceNeeded = dropdownWidth + gap + edgePadding;
let position = {
top: rect.top + scrollY,
left: rect.right + gap + scrollX,
direction: 'right',
verticalDirection: 'down',
2026-01-27 18:27:29 +05:30
};
// 🎯 FIXED: HORIZONTAL POSITIONING - Exactly like your images
if (level === 0) {
console.log('🎯 Space Analysis:', {
spaceRight,
spaceLeft,
spaceNeeded,
elementPosition: { left: rect.left, right: rect.right },
2026-01-27 18:27:29 +05:30
});
if (spaceRight >= spaceNeeded) {
// Image 1: Enough space on right - open to RIGHT
position.left = rect.right + gap + scrollX;
position.direction = 'right';
console.log('✅ Opening RIGHT - space available');
} else if (spaceLeft >= spaceNeeded) {
// Image 2: No space on right, space on left - open to LEFT
position.left = rect.left - dropdownWidth - gap + scrollX;
position.direction = 'left';
} else {
// Edge case: Limited space on both sides
if (spaceRight >= spaceLeft) {
position.left = viewportWidth - dropdownWidth - edgePadding + scrollX;
position.direction = 'right-edge';
} else {
position.left = edgePadding + scrollX;
position.direction = 'left-edge';
}
console.log('⚠️ Edge positioning applied');
}
} else {
// Nested menu positioning
const parentKeys = Object.keys(dropdownPositions);
const parentDirection =
parentKeys.length > 0
? dropdownPositions[parentKeys[level - 1]]?.direction || 'right'
: 'right';
2026-01-27 18:27:29 +05:30
if (parentDirection.includes('right')) {
if (spaceRight >= spaceNeeded) {
position.left = rect.right + gap + scrollX;
position.direction = 'right';
} else {
position.left = rect.left - dropdownWidth - gap + scrollX;
position.direction = 'left';
}
} else {
if (spaceLeft >= spaceNeeded) {
position.left = rect.left - dropdownWidth - gap + scrollX;
position.direction = 'left';
} else {
position.left = rect.right + gap + scrollX;
position.direction = 'right';
}
}
}
// 🎯 FIXED: VERTICAL POSITIONING
const spaceBelow = viewportHeight - rect.bottom;
const spaceAbove = rect.top;
if (isBottomMenu && level === 0) {
if (spaceAbove >= dropdownHeight + edgePadding) {
position.top = rect.top - dropdownHeight - gap + scrollY;
position.verticalDirection = 'up';
} else {
position.top = rect.bottom + gap + scrollY;
position.verticalDirection = 'down';
}
} else {
if (spaceBelow >= dropdownHeight + edgePadding) {
position.top = rect.top + scrollY;
position.verticalDirection = 'down';
} else if (spaceAbove >= dropdownHeight + edgePadding) {
position.top = rect.bottom - dropdownHeight + scrollY;
position.verticalDirection = 'up';
} else {
position.top =
Math.max(edgePadding, (viewportHeight - dropdownHeight) / 2) +
scrollY;
2026-01-27 18:27:29 +05:30
position.verticalDirection = 'center';
}
}
// 🎯 FIXED: Final boundary checks
position.left = Math.max(
edgePadding + scrollX,
Math.min(
position.left,
viewportWidth - dropdownWidth - edgePadding + scrollX
)
2026-01-27 18:27:29 +05:30
);
position.top = Math.max(
edgePadding + scrollY,
Math.min(
position.top,
viewportHeight - dropdownHeight - edgePadding + scrollY
)
2026-01-27 18:27:29 +05:30
);
// Store dimensions for reference
position.width = dropdownWidth;
position.height = dropdownHeight;
console.log('🎯 FIXED Position:', {
clicked: {
top: rect.top,
left: rect.left,
right: rect.right,
bottom: rect.bottom,
},
spaces: {
right: spaceRight,
left: spaceLeft,
above: spaceAbove,
below: spaceBelow,
},
2026-01-27 18:27:29 +05:30
calculated: position,
direction: position.direction,
2026-01-27 18:27:29 +05:30
});
return position;
};
// Handle menu item click
const handleMenuClick = (item, event, parentKeys = []) => {
if (item?.label === 'Branch Signin') {
2026-03-10 15:49:03 +05:30
if (BranchId) {
sessionStorage.removeItem('BranchId');
2026-03-10 15:49:03 +05:30
}
}
2026-01-27 18:27:29 +05:30
event.stopPropagation();
if (item.children && item.children.length > 0) {
// Has children - toggle submenu
const itemKey = item.key;
const isOpen = openKeys.includes(itemKey);
if (isOpen) {
// Close this submenu and all its children
setOpenKeys((prev) =>
prev.filter((key) => {
// Keep only keys that are not this item or its descendants
return !key.startsWith(itemKey) && key !== itemKey;
})
);
setDropdownPositions((prev) => {
2026-01-27 18:27:29 +05:30
const newPos = { ...prev };
Object.keys(newPos).forEach((key) => {
2026-01-27 18:27:29 +05:30
if (key.startsWith(itemKey) || key === itemKey) {
delete newPos[key];
}
});
return newPos;
});
} else {
// Close all submenus at the same level or deeper
const currentPath = [...parentKeys, itemKey];
const newOpenKeys = openKeys.filter((key) => {
2026-01-27 18:27:29 +05:30
// Keep only parent keys in the current path
return (
currentPath.some((pathKey) => key === pathKey) ||
2026-01-27 18:27:29 +05:30
currentPath.every((pathKey, index) => {
const keyParts = key.split('-');
const pathParts = pathKey.split('-');
return (
index < keyParts.length && keyParts[index] === pathParts[index]
);
})
);
2026-01-27 18:27:29 +05:30
});
// Add current item to open keys
newOpenKeys.push(itemKey);
// Calculate position and add to dropdown positions
const position = calculatePosition(
event.currentTarget,
parentKeys.length
);
2026-01-27 18:27:29 +05:30
setDropdownPositions((prev) => {
2026-01-27 18:27:29 +05:30
// Remove positions for closed items
const newPos = {};
newOpenKeys.forEach((key) => {
2026-01-27 18:27:29 +05:30
if (prev[key]) newPos[key] = prev[key];
});
newPos[itemKey] = position;
return newPos;
});
setOpenKeys(newOpenKeys);
}
} else {
// No children - select item and close all dropdowns
setSelectedKey(item.key);
setOpenKeys([]);
setDropdownPositions({});
// Fullscreen on Sales Entry click
if (item.label === 'Sales Entry') {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(() => {});
}
}
2026-01-27 18:27:29 +05:30
// Trigger onClick callback if provided
if (onClick) {
onClick({
key: item.key,
keyPath: [...parentKeys, item.key],
item: item,
domEvent: event,
2026-01-27 18:27:29 +05:30
});
}
}
};
// Check if item is selected
const isSelected = (key) => selectedKey === key;
// Check if item is open
const isOpen = (key) => openKeys.includes(key);
// Check if item is in active path (has selected child)
const isInActivePath = (item, parentKeys = []) => {
if (!item.children) return false;
const checkChildren = (children, currentPath) => {
return children.some((child) => {
2026-01-27 18:27:29 +05:30
const childPath = [...currentPath, child.key];
if (child.key === selectedKey) return true;
if (child.children) {
return checkChildren(child.children, childPath);
}
return false;
});
};
return checkChildren(item.children, [...parentKeys, item.key]);
};
const menuRefs = useRef({});
const submenuRefs = useRef({});
const [positions, setPositions] = useState({});
const renderMenuItems = (items, parentKeys = [], level = 0) => {
return items.map((item) => {
if (!item) return null;
const hasChildren = item.children && item.children.length > 0;
const isItemSelected = isSelected(item.key);
const isItemOpen = isOpen(item.key);
const isItemInActivePath = isInActivePath(item, parentKeys);
const currentKeys = [...parentKeys, item.key];
return (
<div
key={item.key}
className="pozo-menu-item-wrapper"
ref={(el) => {
if (el) menuRefs.current[item.key] = el;
}}
>
<div
className={`pozo-menu-item ${isItemSelected ? 'selected' : ''} ${isItemOpen ? 'open' : ''} ${isItemInActivePath ? 'active-path' : ''} ${hasChildren ? 'has-children' : ''} ${item.type === 'group' ? 'group-item' : ''}`}
onClick={(e) => handleMenuClick(item, e, parentKeys)}
data-menu-key={item.key}
style={{ justifyContent: 'flex-start' }}
2026-01-27 18:27:29 +05:30
>
{item.icon && (
<span
className="pozo-menu-icon"
style={{ fontSize: collapse ? '1.3rem' : '' }}
>
2026-01-27 18:27:29 +05:30
{item.icon}
</span>
)}
{!collapse && <span className="pozo-menu-label">{item.label}</span>}
{hasChildren && !collapse && (
// <span className="pozo-menu-arrow"><FaCaretRight /></span>
<span className={`${isItemOpen ? 'open ' : ''}pozo-menu-arrow`}>
2026-01-27 18:27:29 +05:30
<FaCaretRight />
</span>
)}
</div>
{hasChildren && isItemOpen && !collapse && (
<div
ref={(el) => {
if (el) submenuRefs.current[item.key] = el;
}}
className={`pozo-dropdown-submenu ${level > 0 ? 'nested-submenu' : ''}`}
style={{
position: 'fixed',
top: (() => {
const defaultTop = positions[item.key]?.top || 0;
const submenuHeight =
submenuRefs.current[item.key]?.getBoundingClientRect()
?.height || 0;
2026-01-27 18:27:29 +05:30
const screenHeight = window.innerHeight;
// ✅ If submenu would overflow screen bottom, adjust top
if (defaultTop + submenuHeight > screenHeight) {
return Math.max(
8, // padding from top
screenHeight - submenuHeight - 8
);
}
return defaultTop;
})(),
2026-03-10 15:49:03 +05:30
left: positions[item.key]?.left + 5 || 0,
2026-01-27 18:27:29 +05:30
// width: 220,
width: 200,
maxHeight: 400,
zIndex: 1000 + level,
2026-01-27 18:27:29 +05:30
}}
>
<div className="pozo-submenu-content">
{renderMenuItems(item.children, currentKeys, level + 1)}
</div>
</div>
)}
</div>
);
});
};
// ✅ Compute positions after render
useEffect(() => {
const updated = {};
for (const [key, el] of Object.entries(menuRefs.current)) {
if (el && typeof el.getBoundingClientRect === 'function') {
2026-01-27 18:27:29 +05:30
const rect = el.getBoundingClientRect();
updated[key] = {
top: rect.top + window.scrollY,
left: rect.right + window.scrollX,
2026-01-27 18:27:29 +05:30
};
}
}
Object.entries(submenuRefs.current).forEach(([key, el]) => {
if (el) {
const height = el.getBoundingClientRect().height;
console.log(`Total submenu height for ${key}:`, height);
}
});
setPositions(updated);
}, [items, openKeys]);
return (
<div className={`pozo-menu pozo-menu-${mode}`} style={style}>
2026-01-27 18:27:29 +05:30
<div className="pozo-menu-content" ref={menuRef}>
{renderMenuItems(items)}
</div>
</div>
);
};
export default PozoMenu;