import React, { useContext, useEffect, useState } from 'react'; import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; import { Accordion, AccordionItem, Button, Checkbox } from '@carbon/react'; import { useConfig, useLayoutType } from '@openmrs/esm-framework'; import { type ConfigObject } from '../../config-schema'; import type { FilterNodeProps, FilterLeafProps } from './filter-types'; import FilterEmptyState from '../ui-elements/reset-filters-empty-state/filter-empty-state.component'; import FilterContext from './filter-context'; import styles from './filter-set.scss'; const isIndeterminate = (kids: Array, checkboxes: Record) => { return kids && !kids?.every((kid) => checkboxes[kid]) && !kids?.every((kid) => !checkboxes[kid]); }; interface FilterSetProps { hideFilterSetHeader?: boolean; } interface FilterNodeParentProps extends Pick { itemNumber: number; } function filterTreeNode(inputValue: string, treeNode: any): boolean { if (!treeNode) { return false; } const matchesSearch = treeNode.display.toLowerCase().includes(inputValue.toLowerCase()); const childMatches = treeNode.subSets?.some((child) => filterTreeNode(inputValue, child)) ?? false; return matchesSearch || childMatches; } const FilterSet: React.FC = () => { const { roots } = useContext(FilterContext); const config = useConfig(); const [searchTerm, setSearchTerm] = useState(''); const [treeDataFiltered, setTreeDataFiltered] = useState(roots); useEffect(() => { const configuredConceptUuids = config?.resultsViewerConcepts?.map((c) => c.conceptUuid) ?? []; // Helper to check if a node or its children match configured concepts const isConfiguredNode = (node: any): boolean => { // Check if the node itself has a matching conceptUuid if (node.conceptUuid && configuredConceptUuids.includes(node.conceptUuid)) { return true; } // Check if any direct child has a matching conceptUuid return ( node.subSets?.some((child: any) => child.conceptUuid && configuredConceptUuids.includes(child.conceptUuid)) ?? false ); }; // Filter the tree data, ensuring only parent categories (with subSets) are at root level const filteredData = (roots as any[]).filter((node) => { // Only include nodes that have subSets (are parent categories) if (!node.subSets || !Array.isArray(node.subSets)) { return false; } // Keep configured concepts even if they have no children after filtering const isConfiguredConcept = isConfiguredNode(node); if (node.subSets.length === 0 && !isConfiguredConcept) { return false; } // Apply search filter return filterTreeNode(searchTerm, node); }); setTreeDataFiltered(filteredData); }, [searchTerm, roots, config]); return (
{treeDataFiltered?.length > 0 ? ( treeDataFiltered?.map((root, index) => (
)) ) : ( setSearchTerm('')} /> )}
); }; const FilterNodeParent = ({ root, itemNumber }: FilterNodeParentProps) => { const config = useConfig(); const { t } = useTranslation(); const tablet = useLayoutType() === 'tablet'; const [expandAll, setExpandAll] = useState(undefined); if (!root.subSets) { return; } const filterParent = root.subSets.map((node, key) => { // If node has obs data, it's a leaf - render as FilterLeaf if (node?.obs && Array.isArray(node.obs) && node.obs.length > 0) { return (
); } // If node has subSets, it's a parent - render as FilterNode if (node?.subSets && Array.isArray(node.subSets)) { return (
); } // Fallback - treat as leaf return (
); }); const hasChildren = root.subSets && root.subSets.length > 0; return (
{t(root.display)}
{hasChildren && ( )}
{filterParent}
); }; const FilterNode = ({ root, level, open }: FilterNodeProps) => { const tablet = useLayoutType() === 'tablet'; const { checkboxes, parents, updateParent, toggleVal } = useContext(FilterContext); const indeterminate = isIndeterminate(parents[root.flatName], checkboxes); const allChildrenChecked = parents[root.flatName]?.every((kid) => checkboxes[kid]); // Determine if this is a parent (has children) or leaf (individual test) const isParent = parents[root.flatName] && parents[root.flatName].length > 0; return ( (isParent ? updateParent(root.flatName) : toggleVal(root.flatName))} disabled={!root.hasData} /> } // @ts-ignore style={{ paddingLeft: `${level > 0 ? 1 : 0}rem` }} open={open ?? false} > {/* If this node has obs data, it's a leaf - render as FilterLeaf */} {root?.obs && Array.isArray(root.obs) && root.obs.length > 0 ? ( ) : root?.subSets && Array.isArray(root.subSets) && root.subSets.length > 0 ? ( /* If this node has subSets, it's a parent - recursively render children */ root.subSets.map((child, index) => { /* Check if child is a leaf (has obs) or parent (has subSets) */ if (child?.obs && Array.isArray(child.obs) && child.obs.length > 0) { /* Child is a leaf - render as FilterLeaf */ return ; } else if (child?.subSets && Array.isArray(child.subSets) && child.subSets.length > 0) { /* Child is a parent - recursively render as FilterNode */ return ; } else { /* Child has no obs and no subSets - treat as leaf */ return ; } }) ) : ( /* Parent node with empty subSets - render empty div to maintain accordion structure */
)} ); }; const FilterLeaf = ({ leaf }: FilterLeafProps) => { const { checkboxes, toggleVal } = useContext(FilterContext); return (
toggleVal(leaf.flatName)} disabled={!leaf.hasData} />
); }; export default FilterSet;