'use client'; import { useState, useEffect, useMemo, useRef, type ReactNode } from 'react'; import { Tag, Plus, Search, RefreshCw, ChevronDown, ChevronRight, 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, BrandResponse } from '@/services/api/catalogService'; import { getMediaUrl } from '@/services/api/config'; 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'; const DEVICE_TYPES = ['laptop', 'tablet', 'mobile'] as const; type DeviceType = (typeof DEVICE_TYPES)[number]; type StatusFilter = 'active' | 'inactive'; type SortField = 'name' | 'slug' | 'status'; type SortDir = 'asc' | 'desc'; type SortConfig = { field: SortField; dir: SortDir }; type FilterSection = 'name' | 'deviceTypes' | 'status'; type BrandFilters = { brandIds: string[]; deviceTypes: DeviceType[]; statuses: StatusFilter[]; }; const EMPTY_FILTERS: BrandFilters = { brandIds: [], deviceTypes: [], statuses: [], }; const FILTER_PAGE_SIZE = 5; const EMPTY_COLUMNS = { brandName: true, slug: true, deviceTypes: true, status: true, actions: true, }; const COLUMN_OPTIONS = [ { key: 'brandName' as const, label: 'Brand Name', locked: true }, { key: 'slug' as const, label: 'Slug / Path', locked: false }, { key: 'deviceTypes' as const, label: 'Device Types', 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: 'status', dir: 'asc', label: 'Status Active first' }, { field: 'status', dir: 'desc', label: 'Status Inactive first' }, ]; const DEFAULT_SORT: SortConfig = { field: 'name', dir: 'asc' }; const DEFAULT_DATE_FROM = format(subDays(new Date(), 30), 'yyyy-MM-dd'); const DEFAULT_DATE_TO = format(new Date(), 'yyyy-MM-dd'); function normalizeId(value: unknown): string { if (value == null) return ''; return String(value).trim(); } 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 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(); } function formatDeviceTypes(types?: string[]): string { if (!types || types.length === 0) return '—'; return types.map((t) => t.charAt(0).toUpperCase() + t.slice(1)).join(', '); } export default function BrandsPage() { const [brands, setBrands] = 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, deviceTypes: true, status: false, }); const [filterSectionSearch, setFilterSectionSearch] = useState({ name: '' }); const [filterVisibleCounts, setFilterVisibleCounts] = useState({ name: 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); const [showBrandModal, setShowBrandModal] = useState(false); const [editingBrand, setEditingBrand] = useState(null); const [brandName, setBrandName] = useState(''); const [brandLogo, setBrandLogo] = useState(''); const [selectedDeviceTypes, setSelectedDeviceTypes] = useState([]); const [hasMultipleDevices, setHasMultipleDevices] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [deleteTarget, setDeleteTarget] = useState<{ id: string; name: string } | null>(null); const [isDeleting, setIsDeleting] = useState(false); const fetchData = async () => { setLoading(true); try { const fetchedBrands = await catalogService.getBrands(); setBrands(fetchedBrands); } catch (err: any) { toast.error(err?.message || 'Failed to load brand 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 = () => { setEditingBrand(null); setBrandName(''); setBrandLogo(''); setSelectedDeviceTypes([]); setHasMultipleDevices(false); setShowBrandModal(true); }; const handleOpenEdit = (brand: BrandResponse) => { setEditingBrand(brand); setBrandName(brand.name); setBrandLogo(brand.logo_url || ''); setSelectedDeviceTypes(brand.device_types || []); setHasMultipleDevices((brand.device_types || []).length > 0); setShowBrandModal(true); }; const closeBrandDrawer = () => { if (isSubmitting) return; setShowBrandModal(false); }; const handleSubmitBrand = async (e: React.FormEvent) => { e.preventDefault(); if (!brandName.trim()) { toast.error('Brand name is required'); return; } setIsSubmitting(true); try { const payloadDeviceTypes = hasMultipleDevices ? selectedDeviceTypes : []; if (editingBrand) { const updated = await catalogService.updateBrand(editingBrand.brand_id, { name: brandName.trim(), logo_url: brandLogo ? brandLogo.trim() : '', device_types: payloadDeviceTypes, }); setBrands(brands.map((b) => (b.brand_id === updated.brand_id ? updated : b))); toast.success(`Brand "${updated.name}" updated successfully`); } else { const created = await catalogService.createBrand({ name: brandName.trim(), logo_url: brandLogo ? brandLogo.trim() : '', device_types: payloadDeviceTypes, }); setBrands([...brands, created]); toast.success(`Brand "${created.name}" created successfully`); } setBrandName(''); setBrandLogo(''); setSelectedDeviceTypes([]); setHasMultipleDevices(false); setEditingBrand(null); setShowBrandModal(false); } catch (err: any) { toast.error(err?.message || `Failed to ${editingBrand ? 'update' : 'create'} brand`); } 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 { setBrands((prev) => prev.filter((b) => b.brand_id !== id)); await catalogService.deleteBrand(id); toast.success(`Brand "${name}" deleted successfully`); setDeleteTarget(null); fetchData(); } catch (err: any) { toast.error(err?.message || 'Failed to delete brand'); fetchData(); } finally { setIsDeleting(false); } }; const filteredBrands = useMemo(() => { const q = searchQuery.trim().toLowerCase(); const fromTs = dateFilterEnabled && dateFrom ? dayStartMs(dateFrom) : null; const toTs = dateFilterEnabled && dateTo ? dayEndMs(dateTo) : null; const result = brands.filter((b) => { const searchMatch = !q || (b.name || '').toLowerCase().includes(q) || (b.slug || '').toLowerCase().includes(q); const isActive = coerceFlag(b.is_active); const statusMatch = activeFilters.statuses.length === 0 || (activeFilters.statuses.includes('active') && isActive) || (activeFilters.statuses.includes('inactive') && !isActive); const brandMatch = activeFilters.brandIds.length === 0 || activeFilters.brandIds.includes(normalizeId(b.brand_id)); const brandDeviceTypes = b.device_types || []; const deviceMatch = activeFilters.deviceTypes.length === 0 || activeFilters.deviceTypes.some((dt) => brandDeviceTypes.includes(dt)); let dateMatch = true; if (fromTs != null && toTs != null && !Number.isNaN(fromTs) && !Number.isNaN(toTs)) { const createdTs = new Date(b.created_at).getTime(); if (!Number.isNaN(createdTs)) { dateMatch = createdTs >= fromTs && createdTs <= toTs; } } return searchMatch && brandMatch && statusMatch && deviceMatch && dateMatch; }); const dir = sortConfig.dir === 'asc' ? 1 : -1; return [...result].sort((a, b) => { if (sortConfig.field === 'status') { return (Number(coerceFlag(a.is_active)) - Number(coerceFlag(b.is_active))) * dir; } const left = sortConfig.field === 'name' ? a.name || '' : a.slug || ''; const right = sortConfig.field === 'name' ? b.name || '' : b.slug || ''; return left.localeCompare(right, undefined, { sensitivity: 'base' }) * dir; }); }, [brands, searchQuery, activeFilters, dateFrom, dateTo, dateFilterEnabled, sortConfig]); const pager = useClientPagination(filteredBrands); const filtersActive = activeFilters.brandIds.length > 0 || activeFilters.deviceTypes.length > 0 || activeFilters.statuses.length > 0; const sortedBrandFilterOptions = useMemo( () => [...brands].sort((a, b) => a.name.localeCompare(b.name)), [brands] ); 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: '' }); setFilterVisibleCounts({ name: 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 === 'brandName') return; setVisibleColumns((prev) => ({ ...prev, [key]: !prev[key] })); }; const handleExportExcel = () => { if (filteredBrands.length === 0) { toast.warning('No brands to export'); return; } const headers = ['S.No', 'Brand Name', 'Slug / Path', 'Device Types', 'Status']; const rows = filteredBrands.map((b, i) => [ i + 1, b.name, b.slug || '', formatDeviceTypes(b.device_types), coerceFlag(b.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_brands_${format(new Date(), 'yyyyMMdd')}.csv`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); setShowExportMenu(false); toast.success(`Downloaded ${filteredBrands.length} ${filteredBrands.length === 1 ? 'brand' : 'brands'} as CSV`); }; const handleExportPDF = () => { if (filteredBrands.length === 0) { toast.warning('No brands to export'); return; } const printWindow = window.open('', '_blank'); if (!printWindow) { toast.error('Allow pop-ups to open the brands print preview'); return; } const html = ` iFixKart Brands PDF Export

iFixKart Brands (${filteredBrands.length} Records)

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

${filteredBrands.map((b, i) => ` `).join('')}
S.No Brand Name Slug / Path Device Types Status
${i + 1} ${b.name} ${b.slug || ''} ${formatDeviceTypes(b.device_types)} ${coerceFlag(b.is_active) ? 'Active' : 'Inactive'}
`; printWindow.document.write(html); printWindow.document.close(); setShowExportMenu(false); toast.success(`Print preview opened for ${filteredBrands.length} ${filteredBrands.length === 1 ? 'brand' : 'brands'}`); }; 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 renderDeviceTypeBadges = (types?: string[]) => { if (!types || types.length === 0) { return ; } return (
{types.map((dt) => ( {dt} ))}
); }; const renderBrandAvatar = (brand: BrandResponse, size: 'sm' | 'md' = 'md') => { const dim = size === 'sm' ? 'w-9 h-9' : 'w-10 h-10'; return (
{brand.logo_url ? ( {brand.name} ) : ( )}
); }; 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 addBrandButton = ( ); const viewToggle = ; const renderFilterControl = (align: 'left' | 'right') => { const nameQuery = filterSectionSearch.name.trim().toLowerCase(); const filteredNameOptions = sortedBrandFilterOptions.filter((brand) => !nameQuery || brand.name.toLowerCase().includes(nameQuery) || (brand.slug || '').toLowerCase().includes(nameQuery) ); const visibleNameOptions = filteredNameOptions.slice(0, filterVisibleCounts.name); const renderSectionSearch = (value: string, onChange: (next: string) => void) => (
{ onChange(e.target.value); setFilterVisibleCounts({ name: 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(filterSectionSearch.name, (value) => setFilterSectionSearch({ name: value }) )}
{visibleNameOptions.length === 0 ? (

No brands found.

) : ( visibleNameOptions.map((brand) => { const id = normalizeId(brand.brand_id); return renderCheckboxOption( draftFilters.brandIds.includes(id), () => toggleDraftListValue('brandIds', id), brand.name, renderBrandAvatar(brand, 'sm') ); }) )}
{filteredNameOptions.length > visibleNameOptions.length && ( )}
)}
{expandedFilterSections.deviceTypes && (
{DEVICE_TYPES.map((dt) => renderCheckboxOption( draftFilters.deviceTypes.includes(dt), () => toggleDraftListValue('deviceTypes', dt), dt.charAt(0).toUpperCase() + dt.slice(1) ) )}
)}
{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" />
)}
); const emptyMessage = searchQuery || filtersActive || dateFilterEnabled ? 'No brands match your search.' : 'No brands found.'; return (

Brands

{brands.length}
{showExportMenu && (
)}
{viewMode === 'grid' ? (
{renderFilterControl('left')} {searchField}
{viewToggle} {addBrandButton}
{loading ? (
Loading brands...
) : filteredBrands.length === 0 ? (
{emptyMessage}
) : (
{pager.items.map((b) => (
setOpenActionId(open ? b.brand_id : null)} onEdit={() => handleOpenEdit(b)} onDelete={() => openDeleteModal(b.brand_id, b.name)} />
{renderBrandAvatar(b)}

{b.name}

{b.slug}

{renderDeviceTypeBadges(b.device_types)}
{renderStatusBadge(coerceFlag(b.is_active))}
))}
)}
) : (
{searchField} {addBrandButton}
{sortControl} {dateControl}
{renderFilterControl('right')} {manageColumnsControl} {viewToggle}
{loading ? (
Loading brands...
) : (
{visibleColumns.brandName && ( )} {visibleColumns.slug && ( )} {visibleColumns.deviceTypes && ( )} {visibleColumns.status && ( )} {visibleColumns.actions && ( )} {filteredBrands.length === 0 ? ( ) : ( pager.items.map((b) => ( {visibleColumns.brandName && ( )} {visibleColumns.slug && ( )} {visibleColumns.deviceTypes && ( )} {visibleColumns.status && ( )} {visibleColumns.actions && ( )} )) )}
{renderSortLabel('Brand Name', 'name')} {renderSortLabel('Slug / Path', 'slug')} {renderSortLabel('Device Types')} {renderSortLabel('Status', 'status')} {renderSortLabel('Action')}
{emptyMessage}
{renderBrandAvatar(b, 'sm')}

{b.name}

{b.slug}

{b.slug}{renderDeviceTypeBadges(b.device_types)}{renderStatusBadge(coerceFlag(b.is_active))}
setOpenActionId(open ? b.brand_id : null)} onEdit={() => handleOpenEdit(b)} onDelete={() => openDeleteModal(b.brand_id, b.name)} />
)}
)} {deleteTarget && (
e.stopPropagation()} >

Delete Brand

Are you sure you want to delete{' '} "{deleteTarget.name}" and all its related products, models, and series?

)} } >
setBrandName(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" />
{ setHasMultipleDevices(e.target.checked); if (!e.target.checked) setSelectedDeviceTypes([]); }} className="mt-0.5 rounded border-border text-primary focus:ring-primary cursor-pointer" />
{hasMultipleDevices && (
{DEVICE_TYPES.map((dt) => ( ))}
)}
setBrandLogo(url)} onClear={() => setBrandLogo('')} />
); }