import React, { useState, useRef, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; /** * CategorySelector - Selector de categorías colapsable * * Props: * - categories: Array de todas las categorías * - value: ID de la categoría seleccionada * - onChange: Función callback cuando se selecciona una categoría * - onAddNew: Función callback para agregar nueva categoría (opcional) * - placeholder: Texto del placeholder (opcional) * - disabled: Si está deshabilitado (opcional) */ export default function CategorySelector({ categories = [], value, onChange, onAddNew, placeholder, disabled = false }) { const { t } = useTranslation(); const [isOpen, setIsOpen] = useState(false); const [expandedCategories, setExpandedCategories] = useState({}); const [searchTerm, setSearchTerm] = useState(''); const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0, showAbove: false }); const containerRef = useRef(null); const buttonRef = useRef(null); const dropdownRef = useRef(null); const searchInputRef = useRef(null); // Calcular posición del dropdown const updateDropdownPosition = () => { if (buttonRef.current) { const rect = buttonRef.current.getBoundingClientRect(); const isMobile = window.innerWidth < 768; const viewportHeight = window.innerHeight; // Altura máxima do dropdown const maxDropdownHeight = isMobile ? 280 : 350; // Largura mínima para exibir conteúdo completo const minWidth = Math.max(rect.width, isMobile ? 280 : 320); // Ajustar left se ultrapassar viewport let leftPosition = rect.left; const viewportWidth = window.innerWidth; if (leftPosition + minWidth > viewportWidth - 20) { leftPosition = viewportWidth - minWidth - 20; } if (leftPosition < 10) { leftPosition = 10; } // Espaço disponível abaixo e acima const spaceBelow = viewportHeight - rect.bottom - 10; const spaceAbove = rect.top - 10; // Decidir se mostra acima ou abaixo const showAbove = spaceBelow < maxDropdownHeight && spaceAbove > spaceBelow; // Calcular altura real disponível const availableSpace = showAbove ? spaceAbove : spaceBelow; const dropdownHeight = Math.min(maxDropdownHeight, Math.max(200, availableSpace)); setDropdownPosition({ top: showAbove ? rect.top - dropdownHeight : rect.bottom, left: leftPosition, width: minWidth, showAbove: showAbove, maxHeight: dropdownHeight }); } }; // Cerrar dropdown al hacer clic fuera useEffect(() => { const handleClickOutside = (event) => { if (containerRef.current && !containerRef.current.contains(event.target) && dropdownRef.current && !dropdownRef.current.contains(event.target)) { setIsOpen(false); setSearchTerm(''); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); // Actualizar posición cuando se abre y en scroll/resize useEffect(() => { if (isOpen) { updateDropdownPosition(); window.addEventListener('scroll', updateDropdownPosition, true); window.addEventListener('resize', updateDropdownPosition); return () => { window.removeEventListener('scroll', updateDropdownPosition, true); window.removeEventListener('resize', updateDropdownPosition); }; } }, [isOpen]); // Focus en búsqueda al abrir useEffect(() => { if (isOpen && searchInputRef.current) { searchInputRef.current.focus(); } }, [isOpen]); // Obtener categorías padre (sin parent_id) const parentCategories = Array.isArray(categories) ? categories.filter(c => !c.parent_id) : []; // Obtener subcategorías de una categoría padre const getSubcategories = (parentId) => { if (!categories || !Array.isArray(categories)) return []; return categories.filter(c => c.parent_id === parentId); }; // Obtener categoría por ID const getCategoryById = (id) => { if (!id) return null; return categories.find(c => c.id === parseInt(id)); }; // Obtener nombre completo de la categoría seleccionada const getSelectedCategoryName = () => { const category = getCategoryById(value); if (!category) return placeholder || t('common.none'); if (category.parent_id) { const parent = getCategoryById(category.parent_id); return parent ? `${parent.name} → ${category.name}` : category.name; } return category.name; }; // Toggle expansión de categoría const toggleCategory = (categoryId, e) => { e.stopPropagation(); setExpandedCategories(prev => ({ ...prev, [categoryId]: !prev[categoryId] })); }; // Seleccionar categoría const handleSelect = (categoryId, e) => { e.stopPropagation(); onChange({ target: { name: 'category_id', value: categoryId || '' } }); setIsOpen(false); setSearchTerm(''); }; // Filtrar categorías por búsqueda const filterCategories = () => { if (!searchTerm.trim()) return parentCategories; const term = searchTerm.toLowerCase(); const matchingParents = []; const matchingSubcategories = new Set(); parentCategories.forEach(parent => { const parentMatches = parent.name.toLowerCase().includes(term); const subs = getSubcategories(parent.id); const matchingSubs = subs.filter(sub => sub.name.toLowerCase().includes(term)); if (parentMatches || matchingSubs.length > 0) { matchingParents.push(parent); if (matchingSubs.length > 0) { matchingSubcategories.add(parent.id); } } }); return { parents: matchingParents, expanded: matchingSubcategories }; }; const filteredData = searchTerm.trim() ? filterCategories() : { parents: parentCategories, expanded: new Set() }; const displayParents = filteredData.parents || parentCategories; const autoExpanded = filteredData.expanded || new Set(); return (