'use client'; import { useState, useEffect, useMemo, useRef, type ReactNode } from 'react'; import { Plus, Search, RefreshCw, Folder, ChevronDown, ChevronUp, ChevronRight, Link as LinkIcon, Tag, Trash2, Filter, X, Box, FileSpreadsheet, FileText, Columns3, GripVertical, ArrowUpDown, Calendar, AlertTriangle } from 'lucide-react'; import { toast } from 'sonner'; import { format, subDays } from 'date-fns'; import { AnimatePresence, motion } from '@/lib/motion'; import { catalogService, CategoryResponse } from '@/services/api/catalogService'; import { getAccessToken } from '@/services/api/client'; import ImageUpload from '@/components/ui/ImageUpload'; import { SlideOver } from '@/components/ui/SlideOver'; import { RowActionsMenu } from '@/components/ui/RowActionsMenu'; import { ViewModeToggle } from '@/components/ui/ViewModeToggle'; import { TablePagination, useClientPagination } from '@/components/ui/TablePagination'; import { CustomSelect } from '@/components/ui/CustomSelect'; function normalizeId(value: unknown): string { if (value == null) return ''; return String(value).trim(); } function isRootParentId(value: unknown): boolean { const id = normalizeId(value).toLowerCase(); return id === '' || id === 'null' || id === 'undefined' || id === '0' || id === 'root' || id === '- root -' || id === 'none'; } function coerceFlag(value: unknown): boolean { if (typeof value === 'boolean') return value; if (typeof value === 'number') return value !== 0; if (value == null) return false; const normalized = String(value).trim().toLowerCase(); if (['true', '1', 'yes', 'enabled', 'active'].includes(normalized)) return true; if (['false', '0', 'no', 'disabled', 'inactive', '', 'null', 'undefined'].includes(normalized)) return false; return false; } function idsEqual(a: unknown, b: unknown): boolean { const left = normalizeId(a); const right = normalizeId(b); return left !== '' && right !== '' && left === right; } function dayStartMs(isoDate: string): number { const [year, month, day] = isoDate.split('-').map(Number); if (!year || !month || !day) return NaN; return new Date(year, month - 1, day, 0, 0, 0, 0).getTime(); } function dayEndMs(isoDate: string): number { const [year, month, day] = isoDate.split('-').map(Number); if (!year || !month || !day) return NaN; return new Date(year, month - 1, day, 23, 59, 59, 999).getTime(); } type StatusFilter = 'active' | 'inactive'; type FeatureFilter = 'enabled' | 'disabled'; type SortField = 'name' | 'slug' | 'parent' | 'sortOrder' | 'status'; type SortDir = 'asc' | 'desc'; type SortConfig = { field: SortField; dir: SortDir }; type FilterSection = 'name' | 'parent' | 'feature' | 'status'; type CategoryFilters = { categoryIds: string[]; parentIds: string[]; features: FeatureFilter[]; statuses: StatusFilter[]; }; const EMPTY_FILTERS: CategoryFilters = { categoryIds: [], parentIds: [], features: [], statuses: [], }; const FILTER_PAGE_SIZE = 5; const EMPTY_COLUMNS = { categoryName: true, slug: true, parentCategory: true, parentFeature: true, sortOrder: true, status: true, actions: true, }; const COLUMN_OPTIONS = [ { key: 'categoryName' as const, label: 'Category Name', locked: true }, { key: 'slug' as const, label: 'Slug / Path', locked: false }, { key: 'parentCategory' as const, label: 'Parent Category', locked: false }, { key: 'parentFeature' as const, label: 'Parent Feature', locked: false }, { key: 'sortOrder' as const, label: 'Sort Order', locked: false }, { key: 'status' as const, label: 'Status', locked: false }, { key: 'actions' as const, label: 'Actions', locked: false }, ]; const SORT_OPTIONS: { field: SortField; dir: SortDir; label: string }[] = [ { field: 'name', dir: 'asc', label: 'Name A-Z' }, { field: 'name', dir: 'desc', label: 'Name Z-A' }, { field: 'slug', dir: 'asc', label: 'Slug A-Z' }, { field: 'slug', dir: 'desc', label: 'Slug Z-A' }, { field: 'parent', dir: 'asc', label: 'Parent A-Z' }, { field: 'parent', dir: 'desc', label: 'Parent Z-A' }, { field: 'sortOrder', dir: 'asc', label: 'Sort Order Ascending' }, { field: 'sortOrder', dir: 'desc', label: 'Sort Order Descending' }, { field: 'status', dir: 'asc', label: 'Status Active first' }, { field: 'status', dir: 'desc', label: 'Status Inactive first' }, ]; const DEFAULT_SORT: SortConfig = { field: 'sortOrder', dir: 'asc' }; const DEFAULT_DATE_FROM = format(subDays(new Date(), 30), 'yyyy-MM-dd'); const DEFAULT_DATE_TO = format(new Date(), 'yyyy-MM-dd'); export default function CategoriesPage() { const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [viewMode, setViewMode] = useState<'table' | 'grid'>('table'); const [openActionId, setOpenActionId] = useState(null); const [showFilterPanel, setShowFilterPanel] = useState(false); const [showExportMenu, setShowExportMenu] = useState(false); const [showColumnsPanel, setShowColumnsPanel] = useState(false); const [showSortPanel, setShowSortPanel] = useState(false); const [showDatePanel, setShowDatePanel] = useState(false); const [draftFilters, setDraftFilters] = useState(EMPTY_FILTERS); const [activeFilters, setActiveFilters] = useState(EMPTY_FILTERS); const [expandedFilterSections, setExpandedFilterSections] = useState>({ name: false, parent: false, feature: true, status: false, }); const [filterSectionSearch, setFilterSectionSearch] = useState({ name: '', parent: '' }); const [filterVisibleCounts, setFilterVisibleCounts] = useState({ name: FILTER_PAGE_SIZE, parent: FILTER_PAGE_SIZE }); const [visibleColumns, setVisibleColumns] = useState(EMPTY_COLUMNS); const [sortConfig, setSortConfig] = useState(DEFAULT_SORT); const [dateFrom, setDateFrom] = useState(DEFAULT_DATE_FROM); const [dateTo, setDateTo] = useState(DEFAULT_DATE_TO); const [draftDateFrom, setDraftDateFrom] = useState(DEFAULT_DATE_FROM); const [draftDateTo, setDraftDateTo] = useState(DEFAULT_DATE_TO); const [dateFilterEnabled, setDateFilterEnabled] = useState(false); const filterRef = useRef(null); const exportRef = useRef(null); const columnsRef = useRef(null); const sortRef = useRef(null); const dateRef = useRef(null); // Category Modal States const [showCategoryModal, setShowCategoryModal] = useState(false); const [editingCategory, setEditingCategory] = useState(null); const [categoryName, setCategoryName] = useState(''); const [parentCatId, setParentCatId] = useState(''); const [catDesc, setCatDesc] = useState(''); const [catImg, setCatImg] = useState(''); const [catSortOrder, setCatSortOrder] = useState('0'); const [catParentFeature, setCatParentFeature] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [deleteTarget, setDeleteTarget] = useState<{ id: string; name: string } | null>(null); const [isDeleting, setIsDeleting] = useState(false); // Flyout mega menu config const [flyoutOpen, setFlyoutOpen] = useState(false); const [flyoutLinks, setFlyoutLinks] = useState<{ label: string; url: string }[]>([ { label: 'View All', url: '' }, { label: 'Best Prices', url: '' }, { label: 'New Releases', url: '' }, { label: 'Featured Items', url: '' }, ]); const [flyoutBrands, setFlyoutBrands] = useState<{ label: string; url: string }[]>([ { label: 'Apple Official', url: '/shop?brand=apple' }, { label: 'Samsung Galaxy', url: '/shop?brand=samsung' }, { label: 'Sony Electronics', url: '/shop?brand=sony' }, { label: 'Dell Systems', url: '/shop?brand=dell' }, ]); const [flyoutPromoText, setFlyoutPromoText] = useState('Get 20% OFF'); const [flyoutSaving, setFlyoutSaving] = useState(false); const fetchData = async () => { setLoading(true); try { const fetchedCats = await catalogService.getCategories(); setCategories(fetchedCats); } catch (err: any) { toast.error(err?.message || 'Failed to load category master data'); } finally { setLoading(false); } }; useEffect(() => { fetchData(); }, []); useEffect(() => { if (!showFilterPanel) return; const onPointerDown = (event: MouseEvent) => { if (!filterRef.current?.contains(event.target as Node)) { setShowFilterPanel(false); } }; const onKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') setShowFilterPanel(false); }; document.addEventListener('mousedown', onPointerDown); document.addEventListener('keydown', onKeyDown); return () => { document.removeEventListener('mousedown', onPointerDown); document.removeEventListener('keydown', onKeyDown); }; }, [showFilterPanel]); useEffect(() => { if (!showExportMenu) return; const onPointerDown = (event: MouseEvent) => { if (!exportRef.current?.contains(event.target as Node)) { setShowExportMenu(false); } }; const onKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') setShowExportMenu(false); }; document.addEventListener('mousedown', onPointerDown); document.addEventListener('keydown', onKeyDown); return () => { document.removeEventListener('mousedown', onPointerDown); document.removeEventListener('keydown', onKeyDown); }; }, [showExportMenu]); useEffect(() => { if (!showColumnsPanel) return; const onPointerDown = (event: MouseEvent) => { if (!columnsRef.current?.contains(event.target as Node)) { setShowColumnsPanel(false); } }; const onKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') setShowColumnsPanel(false); }; document.addEventListener('mousedown', onPointerDown); document.addEventListener('keydown', onKeyDown); return () => { document.removeEventListener('mousedown', onPointerDown); document.removeEventListener('keydown', onKeyDown); }; }, [showColumnsPanel]); useEffect(() => { if (!showSortPanel) return; const onPointerDown = (event: MouseEvent) => { if (!sortRef.current?.contains(event.target as Node)) { setShowSortPanel(false); } }; const onKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') setShowSortPanel(false); }; document.addEventListener('mousedown', onPointerDown); document.addEventListener('keydown', onKeyDown); return () => { document.removeEventListener('mousedown', onPointerDown); document.removeEventListener('keydown', onKeyDown); }; }, [showSortPanel]); useEffect(() => { if (!showDatePanel) return; const onPointerDown = (event: MouseEvent) => { if (!dateRef.current?.contains(event.target as Node)) { setShowDatePanel(false); } }; const onKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') setShowDatePanel(false); }; document.addEventListener('mousedown', onPointerDown); document.addEventListener('keydown', onKeyDown); return () => { document.removeEventListener('mousedown', onPointerDown); document.removeEventListener('keydown', onKeyDown); }; }, [showDatePanel]); const closeOverlays = () => { setShowFilterPanel(false); setShowColumnsPanel(false); setShowSortPanel(false); setShowDatePanel(false); }; const handleViewModeChange = (mode: 'table' | 'grid') => { setViewMode(mode); setOpenActionId(null); closeOverlays(); }; const handleOpenCreate = () => { setEditingCategory(null); setCategoryName(''); setParentCatId(''); setCatDesc(''); setCatImg(''); setCatSortOrder('0'); setCatParentFeature(false); setShowCategoryModal(true); }; const handleOpenEdit = (c: CategoryResponse) => { setEditingCategory(c); setCategoryName(c.name); setParentCatId(c.parent_category_id || ''); setCatDesc(c.description || ''); setCatImg(c.image_url || ''); setCatSortOrder(c.sort_order || '0'); setCatParentFeature(coerceFlag(c.is_parent_feature)); setFlyoutOpen(false); // Fetch existing flyout config for this category from storefront API const slug = c.slug; fetch(`/api/v1/storefront/layout/home?region=category_flyout_${slug}`, { cache: 'no-store' }) .then((r) => r.ok ? r.json() : []) .then((data: any[]) => { if (data && data.length > 0) { const meta = data[0]?.metadata_json || {}; if (meta.links) setFlyoutLinks(meta.links); if (meta.brands) setFlyoutBrands(meta.brands); if (meta.promoText) setFlyoutPromoText(meta.promoText); } else { // Reset to defaults with category slug filled in setFlyoutLinks([ { label: 'View All ' + c.name, url: `/shop?category=${slug}` }, { label: 'Best Prices', url: `/shop?category=${slug}&sort=price_asc` }, { label: 'New Releases', url: `/shop?category=${slug}&sort=newest` }, { label: 'Featured Items', url: `/shop?category=${slug}&featured=true` }, ]); setFlyoutBrands([ { label: 'Apple Official', url: '/shop?brand=apple' }, { label: 'Samsung Galaxy', url: '/shop?brand=samsung' }, { label: 'Sony Electronics', url: '/shop?brand=sony' }, { label: 'Dell Systems', url: '/shop?brand=dell' }, ]); setFlyoutPromoText('Get 20% OFF ' + c.name); } }) .catch(() => {}); setShowCategoryModal(true); }; const closeCategoryDrawer = () => { if (isSubmitting) return; setShowCategoryModal(false); }; const handleSubmitCategory = async (e: React.FormEvent) => { e.preventDefault(); if (!categoryName.trim()) { toast.error('Category name is required'); return; } setIsSubmitting(true); try { if (editingCategory) { const updated = await catalogService.updateCategory(editingCategory.category_id, { name: categoryName.trim(), parent_category_id: parentCatId || null, description: catDesc.trim() || '', image_url: catImg ? catImg.trim() : '', sort_order: catSortOrder, is_parent_feature: catParentFeature, }); setCategories(categories.map((c) => (c.category_id === updated.category_id ? updated : c))); toast.success(`Category "${updated.name}" updated successfully`); } else { const created = await catalogService.createCategory({ name: categoryName.trim(), parent_category_id: parentCatId || null, description: catDesc.trim() || '', image_url: catImg ? catImg.trim() : '', sort_order: catSortOrder, is_parent_feature: catParentFeature, }); setCategories([...categories, created]); toast.success(`Category "${created.name}" created successfully`); } setCategoryName(''); setParentCatId(''); setCatDesc(''); setCatImg(''); setCatSortOrder('0'); setCatParentFeature(false); setEditingCategory(null); setShowCategoryModal(false); } catch (err: any) { toast.error(err?.message || `Failed to ${editingCategory ? 'update' : 'create'} category`); } finally { setIsSubmitting(false); } }; const openDeleteModal = (id: string, name: string) => { setOpenActionId(null); setDeleteTarget({ id, name }); }; const closeDeleteModal = () => { if (isDeleting) return; setDeleteTarget(null); }; const handleConfirmDelete = async () => { if (!deleteTarget) return; const { id, name } = deleteTarget; setIsDeleting(true); try { setCategories((prev) => prev.filter((c) => c.category_id !== id)); await catalogService.deleteCategory(id); toast.success(`Category "${name}" deleted successfully`); setDeleteTarget(null); fetchData(); } catch (err: any) { toast.error(err?.message || 'Failed to delete category'); fetchData(); } finally { setIsDeleting(false); } }; const handleSaveFlyout = async () => { if (!editingCategory) return; setFlyoutSaving(true); try { const token = getAccessToken(); const backendUrl = process.env.NEXT_PUBLIC_API_URL || ''; const headers: Record = { 'Content-Type': 'application/json' }; if (token) headers['Authorization'] = `Bearer ${token}`; await fetch(`${backendUrl}/api/v1/admin/storefront/content/bulk`, { method: 'POST', headers, body: JSON.stringify({ items: [{ content_id: null, page: 'home', region: `category_flyout_${editingCategory.slug}`, type: 'category_flyout', title: `${editingCategory.name} Flyout Menu`, subtitle: '', display_order: 0, metadata_json: { links: flyoutLinks, brands: flyoutBrands, promoText: flyoutPromoText, } }] }) }); toast.success('Category flyout menu published to the storefront'); } catch (err: any) { toast.error('Could not save the category flyout menu'); } finally { setFlyoutSaving(false); } }; const getParentLabel = (category: CategoryResponse) => { if (isRootParentId(category.parent_category_id)) return '- Root -'; const parent = categories.find((parentCat) => idsEqual(parentCat.category_id, category.parent_category_id)); return parent?.name || '- Root -'; }; const filteredCategories = useMemo(() => { const q = searchQuery.trim().toLowerCase(); const fromTs = dateFilterEnabled && dateFrom ? dayStartMs(dateFrom) : null; const toTs = dateFilterEnabled && dateTo ? dayEndMs(dateTo) : null; const result = categories.filter((c) => { const parent = categories.find((parentCat) => idsEqual(parentCat.category_id, c.parent_category_id)); const searchMatch = !q || (c.name || '').toLowerCase().includes(q) || (c.slug || '').toLowerCase().includes(q) || (c.description || '').toLowerCase().includes(q) || (parent?.name || '').toLowerCase().includes(q); const isActive = coerceFlag(c.is_active); const statusMatch = activeFilters.statuses.length === 0 || (activeFilters.statuses.includes('active') && isActive) || (activeFilters.statuses.includes('inactive') && !isActive); const isRoot = isRootParentId(c.parent_category_id); const parentMatch = activeFilters.parentIds.length === 0 || (isRoot && activeFilters.parentIds.includes('root')) || activeFilters.parentIds.some((id) => id !== 'root' && idsEqual(c.parent_category_id, id)); const parentFeatureEnabled = coerceFlag(c.is_parent_feature); const featureMatch = activeFilters.features.length === 0 || (activeFilters.features.includes('enabled') && parentFeatureEnabled) || (activeFilters.features.includes('disabled') && !parentFeatureEnabled); const categoryMatch = activeFilters.categoryIds.length === 0 || activeFilters.categoryIds.includes(normalizeId(c.category_id)); let dateMatch = true; if (fromTs != null && toTs != null && !Number.isNaN(fromTs) && !Number.isNaN(toTs)) { const createdTs = new Date(c.created_at).getTime(); if (!Number.isNaN(createdTs)) { dateMatch = createdTs >= fromTs && createdTs <= toTs; } } return searchMatch && categoryMatch && statusMatch && parentMatch && featureMatch && dateMatch; }); const dir = sortConfig.dir === 'asc' ? 1 : -1; return [...result].sort((a, b) => { if (sortConfig.field === 'sortOrder') { return (parseInt(a.sort_order || '0', 10) - parseInt(b.sort_order || '0', 10)) * dir; } if (sortConfig.field === 'status') { return (Number(coerceFlag(a.is_active)) - Number(coerceFlag(b.is_active))) * dir; } const parentA = getParentLabel(a); const parentB = getParentLabel(b); const left = sortConfig.field === 'name' ? a.name || '' : sortConfig.field === 'slug' ? a.slug || '' : parentA; const right = sortConfig.field === 'name' ? b.name || '' : sortConfig.field === 'slug' ? b.slug || '' : parentB; return left.localeCompare(right, undefined, { sensitivity: 'base' }) * dir; }); }, [categories, searchQuery, activeFilters, dateFrom, dateTo, dateFilterEnabled, sortConfig]); const pager = useClientPagination(filteredCategories); const parentCategoryOptions = useMemo(() => { const usedParentIds = new Set( categories .map((c) => normalizeId(c.parent_category_id)) .filter((id) => id && !isRootParentId(id)) ); const availableParents = categories.filter((c) => usedParentIds.has(normalizeId(c.category_id))); const source = availableParents.length > 0 ? availableParents : categories; return [...source].sort((a, b) => a.name.localeCompare(b.name)); }, [categories]); const filtersActive = activeFilters.categoryIds.length > 0 || activeFilters.parentIds.length > 0 || activeFilters.features.length > 0 || activeFilters.statuses.length > 0; const sortedCategoryFilterOptions = useMemo( () => [...categories].sort((a, b) => a.name.localeCompare(b.name)), [categories] ); const parentFilterOptions = useMemo(() => { const rootOption = { id: 'root', name: 'Root Category' }; const parents = parentCategoryOptions.map((cat) => ({ id: normalizeId(cat.category_id), name: cat.name, })); return [rootOption, ...parents]; }, [parentCategoryOptions]); const toggleFilterSection = (section: FilterSection) => { setExpandedFilterSections((prev) => ({ ...prev, [section]: !prev[section] })); }; const toggleDraftListValue = ( key: keyof Pick, value: T ) => { setDraftFilters((prev) => { const current = prev[key] as T[]; return { ...prev, [key]: current.includes(value) ? current.filter((item) => item !== value) : [...current, value], }; }); }; const openFilterPanel = () => { setDraftFilters(activeFilters); setFilterSectionSearch({ name: '', parent: '' }); setFilterVisibleCounts({ name: FILTER_PAGE_SIZE, parent: FILTER_PAGE_SIZE }); setShowColumnsPanel(false); setShowSortPanel(false); setShowDatePanel(false); setShowFilterPanel(true); }; const visibleColumnCount = Object.values(visibleColumns).filter(Boolean).length; const toggleColumn = (key: keyof typeof EMPTY_COLUMNS) => { if (key === 'categoryName') return; setVisibleColumns((prev) => ({ ...prev, [key]: !prev[key] })); }; const handleExportExcel = () => { if (filteredCategories.length === 0) { toast.warning('No categories to export'); return; } const headers = ['S.No', 'Category Name', 'Slug / Path', 'Parent Category', 'Parent Feature', 'Sort Order', 'Status']; const rows = filteredCategories.map((c, i) => [ i + 1, c.name, c.slug || '', getParentLabel(c), coerceFlag(c.is_parent_feature) ? 'Enabled' : 'Disabled', c.sort_order || '0', coerceFlag(c.is_active) ? 'Active' : 'Inactive', ]); const csvContent = [headers.join(','), ...rows.map((row) => row.map((val) => `"${String(val).replace(/"/g, '""')}"`).join(','))].join('\n'); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `ifixkart_categories_${format(new Date(), 'yyyyMMdd')}.csv`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); setShowExportMenu(false); toast.success(`Downloaded ${filteredCategories.length} ${filteredCategories.length === 1 ? 'category' : 'categories'} as CSV`); }; const handleExportPDF = () => { if (filteredCategories.length === 0) { toast.warning('No categories to export'); return; } const printWindow = window.open('', '_blank'); if (!printWindow) { toast.error('Allow pop-ups to open the categories print preview'); return; } const html = ` iFixKart Categories PDF Export

iFixKart Categories (${filteredCategories.length} Records)

Exported on: ${format(new Date(), 'yyyy-MM-dd HH:mm:ss')}

${filteredCategories.map((c, i) => ` `).join('')}
S.No Category Name Slug / Path Parent Category Parent Feature Sort Order Status
${i + 1} ${c.name} ${c.slug || ''} ${getParentLabel(c)} ${coerceFlag(c.is_parent_feature) ? 'Enabled' : 'Disabled'} ${c.sort_order || '0'} ${coerceFlag(c.is_active) ? 'Active' : 'Inactive'}
`; printWindow.document.write(html); printWindow.document.close(); setShowExportMenu(false); toast.success(`Print preview opened for ${filteredCategories.length} ${filteredCategories.length === 1 ? 'category' : 'categories'}`); }; const dateRangeLabel = `${format(new Date(dayStartMs(dateFrom || DEFAULT_DATE_FROM)), 'd MMM yy')} - ${format(new Date(dayStartMs(dateTo || DEFAULT_DATE_TO)), 'd MMM yy')}`; const dataCardShell = 'crm-radius-section border border-border bg-card shadow-[0_1px_3px_rgba(16,24,40,0.06)] overflow-hidden min-w-0'; const renderSortLabel = (label: string, field?: SortField, showIcon = true) => { const active = field != null && sortConfig.field === field; return ( {label} {showIcon && ( )} ); }; const renderStatusBadge = (active: boolean) => ( {active ? 'Active' : 'Inactive'} ); const renderFeatureBadge = (enabled: boolean) => ( {enabled ? 'Enabled' : 'Disabled'} ); const searchField = (
setSearchQuery(e.target.value)} className="w-full h-9 pl-9 pr-3 crm-radius-control border border-border bg-card text-[13px] text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-primary transition-colors" />
); const addCategoryButton = ( ); const viewToggle = ( ); const renderFilterControl = (align: 'left' | 'right') => { const nameQuery = filterSectionSearch.name.trim().toLowerCase(); const parentQuery = filterSectionSearch.parent.trim().toLowerCase(); const filteredNameOptions = sortedCategoryFilterOptions.filter((cat) => !nameQuery || cat.name.toLowerCase().includes(nameQuery) || (cat.slug || '').toLowerCase().includes(nameQuery) ); const filteredParentOptions = parentFilterOptions.filter((option) => !parentQuery || option.name.toLowerCase().includes(parentQuery) ); const visibleNameOptions = filteredNameOptions.slice(0, filterVisibleCounts.name); const visibleParentOptions = filteredParentOptions.slice(0, filterVisibleCounts.parent); const renderSectionSearch = ( section: 'name' | 'parent', value: string, onChange: (next: string) => void ) => (
{ onChange(e.target.value); setFilterVisibleCounts((prev) => ({ ...prev, [section]: FILTER_PAGE_SIZE })); }} className="w-full h-8 pl-8 pr-3 crm-radius-control border border-border bg-card text-[12px] text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-primary" />
); const renderCheckboxOption = ( checked: boolean, onToggle: () => void, label: string, icon?: ReactNode ) => ( ); return (
{showFilterPanel && (
Filter
{expandedFilterSections.name && (
{renderSectionSearch('name', filterSectionSearch.name, (value) => setFilterSectionSearch((prev) => ({ ...prev, name: value })) )}
{visibleNameOptions.length === 0 ? (

No categories found.

) : ( visibleNameOptions.map((cat) => { const id = normalizeId(cat.category_id); return renderCheckboxOption( draftFilters.categoryIds.includes(id), () => toggleDraftListValue('categoryIds', id), cat.name, (
) ); }) )}
{filteredNameOptions.length > visibleNameOptions.length && ( )}
)}
{expandedFilterSections.parent && (
{renderSectionSearch('parent', filterSectionSearch.parent, (value) => setFilterSectionSearch((prev) => ({ ...prev, parent: value })) )}
{visibleParentOptions.length === 0 ? (

No parent categories found.

) : ( visibleParentOptions.map((option) => renderCheckboxOption( draftFilters.parentIds.includes(option.id), () => toggleDraftListValue('parentIds', option.id), option.name ) ) )}
{filteredParentOptions.length > visibleParentOptions.length && ( )}
)}
{expandedFilterSections.feature && (
{renderCheckboxOption( draftFilters.features.includes('enabled'), () => toggleDraftListValue('features', 'enabled'), 'Enabled' )} {renderCheckboxOption( draftFilters.features.includes('disabled'), () => toggleDraftListValue('features', 'disabled'), 'Disabled' )}
)}
{expandedFilterSections.status && (
{renderCheckboxOption( draftFilters.statuses.includes('active'), () => toggleDraftListValue('statuses', 'active'), 'Active' )} {renderCheckboxOption( draftFilters.statuses.includes('inactive'), () => toggleDraftListValue('statuses', 'inactive'), 'Inactive' )}
)}
)}
); }; const manageColumnsControl = (
{showColumnsPanel && (
{COLUMN_OPTIONS.map((col) => (
{col.label}
))}
)}
); const sortControl = (
{showSortPanel && (
{SORT_OPTIONS.map((option) => { const active = option.field === sortConfig.field && option.dir === sortConfig.dir; return ( ); })}
)}
); const dateControl = (
{showDatePanel && (
setDraftDateFrom(e.target.value)} className="w-full h-8 px-2.5 crm-radius-control border border-border bg-card text-[13px] text-foreground outline-none focus:border-primary" />
setDraftDateTo(e.target.value)} className="w-full h-8 px-2.5 crm-radius-control border border-border bg-card text-[13px] text-foreground outline-none focus:border-primary" />
)}
); return (

Categories

{categories.length}
{showExportMenu && (
)}
{viewMode === 'grid' ? (
{renderFilterControl('left')} {searchField}
{viewToggle} {addCategoryButton}
{loading ? (
Loading catalog categories...
) : filteredCategories.length === 0 ? (
{searchQuery || filtersActive || dateFilterEnabled ? 'No categories match your search.' : 'No categories found.'}
) : (
{pager.items.map((c) => { const parent = categories.find((parentCat) => idsEqual(parentCat.category_id, c.parent_category_id)); return (
setOpenActionId(open ? c.category_id : null)} onEdit={() => handleOpenEdit(c)} onDelete={() => openDeleteModal(c.category_id, c.name)} />

{c.name}

{c.slug}

Parent: {parent && !isRootParentId(c.parent_category_id) ? parent.name : '- Root -'}

{coerceFlag(c.is_parent_feature) ? 'Parent Feature enabled' : 'Parent Feature disabled'}

{renderStatusBadge(coerceFlag(c.is_active))}
); })}
)}
) : (
{searchField} {addCategoryButton}
{sortControl} {dateControl}
{renderFilterControl('right')} {manageColumnsControl} {viewToggle}
{loading ? (
Loading catalog categories...
) : (
{visibleColumns.categoryName && ( )} {visibleColumns.slug && ( )} {visibleColumns.parentCategory && ( )} {visibleColumns.parentFeature && ( )} {visibleColumns.sortOrder && ( )} {visibleColumns.status && ( )} {visibleColumns.actions && ( )} {filteredCategories.length === 0 ? ( ) : ( pager.items.map((c) => { const parent = categories.find((parentCat) => idsEqual(parentCat.category_id, c.parent_category_id)); const parentLabel = parent && !isRootParentId(c.parent_category_id) ? parent.name : '- Root -'; return ( {visibleColumns.categoryName && ( )} {visibleColumns.slug && ( )} {visibleColumns.parentCategory && ( )} {visibleColumns.parentFeature && ( )} {visibleColumns.sortOrder && ( )} {visibleColumns.status && ( )} {visibleColumns.actions && ( )} ); }) )}
{renderSortLabel('Category Name', 'name')} {renderSortLabel('Slug / Path', 'slug')} {renderSortLabel('Parent Category', 'parent')} {renderSortLabel('Parent Feature')} {renderSortLabel('Sort Order', 'sortOrder')} {renderSortLabel('Status', 'status')} {renderSortLabel('Action')}
{searchQuery || filtersActive || dateFilterEnabled ? 'No categories match your search.' : 'No categories found.'}

{c.name}

{c.slug}

{c.slug} {parentLabel} {renderFeatureBadge(coerceFlag(c.is_parent_feature))} {c.sort_order}{renderStatusBadge(coerceFlag(c.is_active))}
setOpenActionId(open ? c.category_id : null)} onEdit={() => handleOpenEdit(c)} onDelete={() => openDeleteModal(c.category_id, c.name)} />
)}
)} {deleteTarget && (
e.stopPropagation()} >

Delete Category

Are you sure you want to delete{' '} "{deleteTarget.name}" and all its sub-categories and products?

)} } >
setCategoryName(e.target.value)} className="w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary transition-colors" />
cat.category_id !== editingCategory?.category_id) .map((cat) => ({ value: cat.category_id, label: cat.name })), ]} />