import React, { useState, useMemo } from 'react'; import { ExcelRow } from '../types'; import { useColumns } from '../contexts/ColumnsContext'; import { Search, Filter, Edit2, ChevronDown, ChevronUp, X, Maximize2 } from 'lucide-react'; import { cn } from '../lib/utils'; import { ColumnFilterPopover } from './ColumnFilterPopover'; import { usePersistentState } from '../contexts/FilterContext'; import { SyncStatusPill } from './SyncStatusPill'; type TabType = 'all' | 'missingLongDE' | 'missingLongEN' | 'missingShortDE' | 'missingShortEN' | 'complete' | 'incomplete'; interface ProductDescriptionsProps { data: ExcelRow[]; headers: string[]; asinColumnIndex: number | null; onEdit: (index: number) => void; rowStatuses: Record; } export function ProductDescriptions({ data, headers, asinColumnIndex, onEdit, rowStatuses }: ProductDescriptionsProps) { const COLUMNS = useColumns(); const [isFullscreen, setIsFullscreen] = useState(false); // Description columns that should only have Present/Missing filters const DESCRIPTION_COLUMNS = [COLUMNS.LONG_DE, COLUMNS.LONG_EN, COLUMNS.SHORT_DE, COLUMNS.SHORT_EN, COLUMNS.DETAILS_DE, COLUMNS.DETAILS_EN]; const [activeTab, setActiveTab] = usePersistentState('descriptions-tab', 'all'); const [search, setSearch] = usePersistentState('descriptions-search', ''); const [lineFilter, setLineFilter] = usePersistentState('descriptions-lineFilter', ''); const [licenseFilter, setLicenseFilter] = usePersistentState('descriptions-licenseFilter', ''); const [sortCol, setSortCol] = usePersistentState('descriptions-sortCol', null); const [sortDesc, setSortDesc] = usePersistentState('descriptions-sortDesc', false); const [pageSize, setPageSize] = usePersistentState('descriptions-pageSize', 100); const [page, setPage] = useState(1); const [columnFilters, setColumnFilters] = usePersistentState>('descriptions-columnFilters', {}); const [openFilterCol, setOpenFilterCol] = useState(null); const [columnWidths, setColumnWidths] = useState>({ [COLUMNS.ARTICLE_NO]: 110, [COLUMNS.ARTICLE_NAME]: 450, // More flexible space for name ...(asinColumnIndex !== null ? { [asinColumnIndex]: 100 } : {}), [COLUMNS.LINE]: 80, [COLUMNS.LICENSE]: 120, [COLUMNS.CLASSIFICATION]: 100, [COLUMNS.LONG_DE]: 110, [COLUMNS.LONG_EN]: 110, [COLUMNS.SHORT_DE]: 110, [COLUMNS.SHORT_EN]: 110, }); const lines = useMemo(() => Array.from(new Set(data.map(r => r[COLUMNS.LINE]).filter(Boolean))), [data]); const licenses = useMemo(() => Array.from(new Set(data.map(r => r[COLUMNS.LICENSE]).filter(Boolean))), [data]); const filteredData = useMemo(() => { let result = data.map((row, index) => ({ row, index })); // Tab filter if (activeTab === 'missingLongDE') result = result.filter(r => !r.row[COLUMNS.LONG_DE]); if (activeTab === 'missingLongEN') result = result.filter(r => !r.row[COLUMNS.LONG_EN]); if (activeTab === 'missingShortDE') result = result.filter(r => !r.row[COLUMNS.SHORT_DE]); if (activeTab === 'missingShortEN') result = result.filter(r => !r.row[COLUMNS.SHORT_EN]); if (activeTab === 'complete') result = result.filter(r => r.row[COLUMNS.LONG_DE] && r.row[COLUMNS.LONG_EN] && r.row[COLUMNS.SHORT_DE] && r.row[COLUMNS.SHORT_EN] ); if (activeTab === 'incomplete') result = result.filter(r => !r.row[COLUMNS.LONG_DE] || !r.row[COLUMNS.LONG_EN] || !r.row[COLUMNS.SHORT_DE] || !r.row[COLUMNS.SHORT_EN] ); // Search filter if (search) { const terms = search.toLowerCase().split(/\s+/).filter(Boolean); if (terms.length > 0) { result = result.filter(r => { const articleNo = String(r.row[COLUMNS.ARTICLE_NO] || '').toLowerCase(); const articleName = String(r.row[COLUMNS.ARTICLE_NAME] || '').toLowerCase(); return terms.every(term => articleNo.includes(term) || articleName.includes(term)); }); } } // Dropdown filters if (lineFilter) result = result.filter(r => r.row[COLUMNS.LINE] === lineFilter); if (licenseFilter) result = result.filter(r => r.row[COLUMNS.LICENSE] === licenseFilter); // Column-specific filters (Excel-like) Object.entries(columnFilters).forEach(([colIdx, selectedValues]) => { const col = Number(colIdx); const vals = selectedValues as string[]; if (vals.length > 0) { result = result.filter(r => { const cellVal = r.row[col]; if (DESCRIPTION_COLUMNS.includes(col)) { // For description columns, we match synthetic 'Present'/'Missing' values const hasValue = cellVal !== undefined && cellVal !== null && String(cellVal).trim() !== ''; const matchPresent = vals.includes('Present') && hasValue; const matchMissing = vals.includes('Missing') && !hasValue; return matchPresent || matchMissing; } else { // For other columns, use regular value matching with improved empty value handling const cellStr = String(cellVal ?? '').trim(); // If the cell is empty/null/undefined, it matches if 'Empty' or '' is selected return vals.some(v => { const filterVal = String(v ?? '').trim(); return filterVal === cellStr; }); } }); } }); // Sorting if (sortCol !== null) { result.sort((a, b) => { const valA = String(a.row[sortCol] || ''); const valB = String(b.row[sortCol] || ''); return sortDesc ? valB.localeCompare(valA) : valA.localeCompare(valB); }); } return result; }, [data, activeTab, search, lineFilter, licenseFilter, columnFilters, sortCol, sortDesc]); const paginatedData = useMemo(() => { const start = (page - 1) * pageSize; return filteredData.slice(start, start + pageSize); }, [filteredData, page, pageSize]); const totalPages = Math.ceil(filteredData.length / pageSize); const handleSort = (col: number) => { if (sortCol === col) { setSortDesc(!sortDesc); } else { setSortCol(col); setSortDesc(false); } }; const handleResize = (colIndex: number, e: React.MouseEvent) => { e.preventDefault(); const startX = e.pageX; const startWidth = columnWidths[colIndex] || 100; const onMouseMove = (moveEvent: MouseEvent) => { const newWidth = Math.max(60, startWidth + (moveEvent.pageX - startX)); setColumnWidths(prev => ({ ...prev, [colIndex]: newWidth })); }; const onMouseUp = () => { window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onMouseUp); }; window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp); }; const getUniqueValues = (col: number) => { // For description columns, return only 'Present' and 'Missing' if (DESCRIPTION_COLUMNS.includes(col)) { return ['Present', 'Missing']; } // For other columns, return actual unique values const values = data.map(r => String(r[col] || '')); return Array.from(new Set(values)).sort(); }; const toggleColumnFilter = (col: number, value: string) => { console.log('[Filter] toggleColumnFilter called, col:', col, 'value:', JSON.stringify(value)); setColumnFilters(prev => { const current = prev[col] || []; const next = current.includes(value) ? current.filter(v => v !== value) : [...current, value]; const updated = { ...prev, [col]: next }; console.log('[Filter] new columnFilters:', updated); return updated; }); setPage(1); }; const setBatchColumnFilter = (col: number, values: string[]) => { setColumnFilters(prev => ({ ...prev, [col]: values })); setPage(1); }; const getRowColor = (row: ExcelRow) => { const fields = [ row[COLUMNS.LONG_DE], row[COLUMNS.LONG_EN], row[COLUMNS.SHORT_DE], row[COLUMNS.SHORT_EN] ]; const filled = fields.filter(Boolean).length; if (filled === 4) return 'bg-green-900/10 hover:bg-green-900/20'; return ''; }; const Badge = ({ content, row }: { content: any, row: ExcelRow }) => { if (content !== undefined && content !== null && content !== '') return ; return ✗ Missing; }; const tabs: { id: TabType; label: string }[] = [ { id: 'all', label: 'All Products' }, { id: 'missingLongDE', label: 'Missing Long DE' }, { id: 'missingLongEN', label: 'Missing Long EN' }, { id: 'missingShortDE', label: 'Missing Short DE' }, { id: 'missingShortEN', label: 'Missing Short EN' }, { id: 'complete', label: 'Complete' }, { id: 'incomplete', label: 'Incomplete' }, ]; return (
{isFullscreen && ( )}
{tabs.map(tab => ( ))}
{ setSearch(e.target.value); setPage(1); }} className="w-full pl-9 pr-10 py-2 bg-slate-900 border border-slate-700 rounded-md text-sm text-white focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500" /> {search && ( )}
{(Object.values(columnFilters) as string[][]).some(v => v.length > 0) && ( )}
{[ { col: COLUMNS.ARTICLE_NO, label: 'Article No.' }, { col: COLUMNS.ARTICLE_NAME, label: 'Article Name' }, ...(asinColumnIndex !== null ? [{ col: asinColumnIndex, label: 'ASIN' }] : []), { col: COLUMNS.LINE, label: 'Line' }, { col: COLUMNS.LICENSE, label: 'License' }, { col: COLUMNS.CLASSIFICATION, label: 'Classification' }, { col: COLUMNS.LONG_DE, label: 'Long DE' }, { col: COLUMNS.LONG_EN, label: 'Long EN' }, { col: COLUMNS.SHORT_DE, label: 'Short DE' }, { col: COLUMNS.SHORT_EN, label: 'Short EN' }, ].map(({ col, label }) => ( ))} {paginatedData.map(({ row, index }) => { const saveStatus = rowStatuses[String(row[COLUMNS.ARTICLE_NO])]; return ( {asinColumnIndex !== null && ( )} ); })} {paginatedData.length === 0 && ( )}
handleSort(col)}> {label} {sortCol === col && ( sortDesc ? : )}
{/* Resizer handle */}
handleResize(col, e)} className="absolute right-0 top-0 bottom-0 w-1 cursor-col-resize hover:bg-blue-500/50 group-hover:bg-slate-700/50 transition-colors z-20" /> {openFilterCol === col && ( toggleColumnFilter(col, val)} onSelectAll={(vals) => setBatchColumnFilter(col, vals)} onClear={() => { setColumnFilters(prev => { const next = { ...prev }; delete next[col]; return next; }); setOpenFilterCol(null); }} onClose={() => setOpenFilterCol(null)} /> )}
Actions
{row[COLUMNS.ARTICLE_NO]} {row[COLUMNS.ARTICLE_NAME]}{row[asinColumnIndex] || '—'}{row[COLUMNS.LINE]} {row[COLUMNS.LICENSE] || '—'} {row[COLUMNS.CLASSIFICATION] || '—'}
No products found matching the criteria.
Showing {Math.min((page - 1) * pageSize + 1, filteredData.length)} to {Math.min(page * pageSize, filteredData.length)} of {filteredData.length} entries
Page {page} of {totalPages || 1}
); }