feat: persistent search and filters across all tabs via FilterContext

This commit is contained in:
Christian Vidal Wolf
2026-04-24 14:19:09 +02:00
parent 29d5994d6a
commit 917bb08d9f
9 changed files with 111 additions and 54 deletions
+4 -1
View File
@@ -4,6 +4,7 @@ import { X } from 'lucide-react';
import { cn } from './lib/utils';
import { AppState, ExcelRow, resolveColumnIndices, COLUMNS } from './types';
import { ColumnsProvider } from './contexts/ColumnsContext';
import { FilterProvider } from './contexts/FilterContext';
import { Sidebar } from './components/Sidebar';
import { TopBar } from './components/TopBar';
import { ProductDescriptions } from './components/ProductDescriptions';
@@ -618,7 +619,8 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
return (
<ColumnsProvider headers={appState.headers}>
<div className="h-screen bg-[#040d1a] text-slate-200 flex flex-col font-sans overflow-hidden">
<FilterProvider>
<div className="h-screen bg-[#040d1a] text-slate-200 flex flex-col font-sans overflow-hidden">
{!isMaximized && (
<TopBar
stats={stats}
@@ -803,6 +805,7 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
/>
)}
</div>
</FilterProvider>
</ColumnsProvider>
);
}
+4 -3
View File
@@ -5,6 +5,7 @@ import { AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, Edit2, Package,
import { cn } from '../lib/utils';
import { ConfirmModal } from './ConfirmModal';
import { ColumnFilterPopover } from './ColumnFilterPopover';
import { usePersistentState } from '../contexts/FilterContext';
interface DimensionsViewProps {
data: ExcelRow[];
@@ -53,9 +54,9 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat
targetGroupKey: string;
selectedIndices: number[];
} | null>(null);
const [search, setSearch] = useState('');
const [lineFilter, setLineFilter] = useState<string[]>([]);
const [classFilter, setClassFilter] = useState<string[]>([]);
const [search, setSearch] = usePersistentState('global-search', '');
const [lineFilter, setLineFilter] = usePersistentState<string[]>('dimensions-lineFilter', []);
const [classFilter, setClassFilter] = usePersistentState<string[]>('dimensions-classFilter', []);
const [openFilter, setOpenFilter] = useState<'line' | 'class' | null>(null);
const [isDragging, setIsDragging] = useState(false);
const [dragStart, setDragStart] = useState<number | null>(null);
+2 -1
View File
@@ -4,6 +4,7 @@ import { getHistory, deleteHistoryEntry, HistoryEntry } from '../lib/supabase';
import { ExcelRow } from '../types';
import { useColumns } from '../contexts/ColumnsContext';
import { cn } from '../lib/utils';
import { usePersistentState } from '../contexts/FilterContext';
interface HistoryViewProps {
headers: string[];
@@ -17,7 +18,7 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp
const [history, setHistory] = useState<HistoryEntry[]>([]);
const [loading, setLoading] = useState(true);
const [expandedId, setExpandedId] = useState<string | null>(null);
const [search, setSearch] = useState('');
const [search, setSearch] = usePersistentState('global-search', '');
const [isFullscreen, setIsFullscreen] = useState(false);
useEffect(() => {
+5 -4
View File
@@ -3,6 +3,7 @@ import { ExcelRow } from '../types';
import { Search, Filter, ChevronDown, ChevronUp, X, Maximize2 } from 'lucide-react';
import { cn } from '../lib/utils';
import { ColumnFilterPopover } from './ColumnFilterPopover';
import { usePersistentState } from '../contexts/FilterContext';
interface MatrixViewProps {
data: ExcelRow[];
@@ -13,11 +14,11 @@ interface MatrixViewProps {
export function MatrixView({ data, headers, rowStatuses }: MatrixViewProps) {
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(25);
const [search, setSearch] = useState('');
const [columnFilters, setColumnFilters] = useState<Record<number, string[]>>({});
const [search, setSearch] = usePersistentState('global-search', '');
const [columnFilters, setColumnFilters] = usePersistentState<Record<number, string[]>>('matrix-columnFilters', {});
const [openFilterCol, setOpenFilterCol] = useState<number | null>(null);
const [sortCol, setSortCol] = useState<number | null>(null);
const [sortDesc, setSortDesc] = useState(false);
const [sortCol, setSortCol] = usePersistentState<number | null>('matrix-sortCol', null);
const [sortDesc, setSortDesc] = usePersistentState('matrix-sortDesc', false);
const [isFullscreen, setIsFullscreen] = useState(false);
const filteredData = useMemo(() => {
+7 -6
View File
@@ -5,6 +5,7 @@ import { Search, ChevronDown, ChevronUp, X, Edit2, Save, Maximize2 } from 'lucid
import { cn } from '../lib/utils';
import { ColumnFilterPopover } from './ColumnFilterPopover';
import { DateFilterPopover } from './DateFilterPopover';
import { usePersistentState } from '../contexts/FilterContext';
interface MissingDataViewProps {
data: ExcelRow[];
@@ -67,14 +68,14 @@ interface EditingState {
export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: MissingDataViewProps) {
const COLUMNS = useColumns();
const [activeTab, setActiveTab] = useState<TabType>('missingLaunch');
const [search, setSearch] = useState('');
const [sortCol, setSortCol] = useState<number | null>(null);
const [sortDesc, setSortDesc] = useState(false);
const [activeTab, setActiveTab] = usePersistentState<TabType>('missingData-tab', 'missingLaunch');
const [search, setSearch] = usePersistentState('global-search', '');
const [sortCol, setSortCol] = usePersistentState<number | null>('missingData-sortCol', null);
const [sortDesc, setSortDesc] = usePersistentState('missingData-sortDesc', false);
const [page, setPage] = useState(1);
const [editing, setEditing] = useState<EditingState | null>(null);
const [columnFilters, setColumnFilters] = useState<Record<number, string[]>>({});
const [dateFilters, setDateFilters] = useState<Record<number, { start: string; end: string }>>({});
const [columnFilters, setColumnFilters] = usePersistentState<Record<number, string[]>>('missingData-columnFilters', {});
const [dateFilters, setDateFilters] = usePersistentState<Record<number, { start: string; end: string }>>('missingData-dateFilters', {});
const [openFilter, setOpenFilter] = useState<number | null>(null);
const [isFullscreen, setIsFullscreen] = useState(false);
const pageSize = 100;
+2 -1
View File
@@ -3,6 +3,7 @@ import { ExcelRow } from '../types';
import { useColumns } from '../contexts/ColumnsContext';
import { Clock, Undo2, Package, Box, DollarSign, FileText, Search, Filter, X, Maximize2 } from 'lucide-react';
import { cn } from '../lib/utils';
import { usePersistentState } from '../contexts/FilterContext';
interface PendingValidationViewProps {
data: ExcelRow[];
@@ -14,7 +15,7 @@ interface PendingValidationViewProps {
export function PendingValidationView({ data, pendingRows, rowStatuses, onRevertRow, onEdit }: PendingValidationViewProps) {
const COLUMNS = useColumns();
const [search, setSearch] = useState('');
const [search, setSearch] = usePersistentState('global-search', '');
const [isFullscreen, setIsFullscreen] = useState(false);
const pendingEntries = Object.entries(pendingRows);
+38 -30
View File
@@ -21,6 +21,7 @@ import {
} from 'lucide-react';
import { cn } from '../lib/utils';
import { ColumnFilterPopover } from './ColumnFilterPopover';
import { usePersistentState } from '../contexts/FilterContext';
interface PricingViewProps {
data: ExcelRow[];
@@ -53,8 +54,8 @@ function findCol(headers: string[], ...keywords: string[]): number {
export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, rowStatuses }: PricingViewProps) {
const COLUMNS = useColumns();
const [filterMode, setFilterMode] = useState<FilterMode>('all_errors');
const [search, setSearch] = useState('');
const [filterMode, setFilterMode] = usePersistentState<FilterMode>('pricing-filterMode', 'all_errors');
const [search, setSearch] = usePersistentState('global-search', '');
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 100;
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({
@@ -78,33 +79,33 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
const [editingCell, setEditingCell] = useState<EditingCell | null>(null);
const [savingCell, setSavingCell] = useState<{ rowIndex: number; colIndex: number } | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
const [lineMultiFilter, setLineMultiFilter] = useState<string[]>([]);
const [classificationFilter, setClassificationFilter] = useState<string[]>([]);
const [productTypeFilter, setProductTypeFilter] = useState<string[]>([]);
const [nameColFilter, setNameColFilter] = useState<{ terms: string[]; op: 'and' | 'or' }>({ terms: [''], op: 'and' });
const [articleNoColFilter, setArticleNoColFilter] = useState<{ terms: string[]; op: 'and' | 'or' }>({ terms: [''], op: 'and' });
const [globalAdvancedFilter, setGlobalAdvancedFilter] = useState<{ terms: string[]; op: 'and' | 'or' }>({ terms: [''], op: 'and' });
const [openFilter, setOpenFilter] = useState<string | null>(null);
const [unitsOuterFilter, setUnitsOuterFilter] = useState<string[]>([]);
const [outerWFilter, setOuterWFilter] = useState<string[]>([]);
const [outerLFilter, setOuterLFilter] = useState<string[]>([]);
const [outerHFilter, setOuterHFilter] = useState<string[]>([]);
const [dynamicColFilters, setDynamicColFilters] = useState<Record<number, string[]>>({});
const [weightIssueFilter, setWeightIssueFilter] = useState<'all' | 'with' | 'without'>('all');
const [editingType, setEditingType] = useState<{ rowIndex: number; value: string } | null>(null);
const [editingLogistic, setEditingLogistic] = useState<{ rowIndex: number; value: string } | null>(null);
const [typeSuggestions, setTypeSuggestions] = useState<string[]>([]);
const typeInputRef = useRef<HTMLInputElement>(null);
const logisticInputRef = useRef<HTMLInputElement>(null);
const [sortConfig, setSortConfig] = useState<{ key: string | number; direction: 'asc' | 'desc' | null }>({ key: null, direction: null });
const [sortConfig, setSortConfig] = usePersistentState<{ key: string | number; direction: 'asc' | 'desc' | null }>('pricing-sortConfig', { key: null, direction: null });
const [isFullscreen, setIsFullscreen] = useState(false);
const [lineMultiFilter, setLineMultiFilter] = usePersistentState<string[]>('pricing-lineFilter', []);
const [classificationFilter, setClassificationFilter] = usePersistentState<string[]>('pricing-classificationFilter', []);
const [productTypeFilter, setProductTypeFilter] = usePersistentState<string[]>('pricing-productTypeFilter', []);
const [nameColFilter, setNameColFilter] = usePersistentState<{ terms: string[]; op: 'and' | 'or' }>('pricing-nameColFilter', { terms: [''], op: 'and' });
const [articleNoColFilter, setArticleNoColFilter] = usePersistentState<{ terms: string[]; op: 'and' | 'or' }>('pricing-articleNoColFilter', { terms: [''], op: 'and' });
const [globalAdvancedFilter, setGlobalAdvancedFilter] = usePersistentState<{ terms: string[]; op: 'and' | 'or' }>('pricing-globalAdvancedFilter', { terms: [''], op: 'and' });
const [unitsOuterFilter, setUnitsOuterFilter] = usePersistentState<string[]>('pricing-unitsOuterFilter', []);
const [outerWFilter, setOuterWFilter] = usePersistentState<string[]>('pricing-outerWFilter', []);
const [outerLFilter, setOuterLFilter] = usePersistentState<string[]>('pricing-outerLFilter', []);
const [outerHFilter, setOuterHFilter] = usePersistentState<string[]>('pricing-outerHFilter', []);
const [dynamicColFilters, setDynamicColFilters] = usePersistentState<Record<number, string[]>>('pricing-dynamicColFilters', {});
const [weightIssueFilter, setWeightIssueFilter] = usePersistentState<'all' | 'with' | 'without'>('pricing-weightIssueFilter', 'all');
const [selectedSearchItems, setSelectedSearchItems] = usePersistentState<Set<string>>('pricing-selectedSearchItems', new Set());
const [openFilter, setOpenFilter] = useState<string | null>(null);
const [isSearchOpen, setIsSearchOpen] = useState(false);
const [selectedSearchItems, setSelectedSearchItems] = useState<Set<string>>(new Set());
const searchDropdownRef = useRef<HTMLDivElement>(null);
// Pinned columns state
const [pinnedColumns, setPinnedColumns] = useState<Set<string>>(new Set(['articleNo', 'articleName']));
const [pinnedColumns, setPinnedColumns] = usePersistentState<Set<string>>('pricing-pinnedColumns', new Set(['articleNo', 'articleName']));
const [showPinPanel, setShowPinPanel] = useState(false);
// Close search dropdown on click outside
@@ -631,7 +632,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
pricingEditableCols.forEach(col => dynamicOrder.push(`prc_${col.index}`));
containerCols.forEach(col => dynamicOrder.push(`con_${col.index}`));
const fullOrder = [...baseOrder, ...dynamicOrder];
const pinnedOrder = fullOrder.filter(k => pinnedColumns.has(k));
return (key: string): number | null => {
if (!pinnedColumns.has(key)) return null;
@@ -641,6 +642,13 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
};
}, [pinnedColumns, pricingEditableCols, containerCols]);
// Header pinned cells need a stacking context above ALL body pinned cells
// so filter popovers (rendered inside <th>) appear over sticky body columns.
const getHeaderStickyRank = (key: string): number | null => {
const rank = getStickyRank(key);
return rank === null ? null : rank + 100;
};
const isPinned = (key: string) => pinnedColumns.has(key);
// ═ Unique values for dynamic pricing columns ═════════════════════════════
@@ -1143,7 +1151,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<table ref={tableRef} className="w-full text-sm border-collapse table-fixed">
<thead className="sticky top-0 z-10 bg-slate-900 border-b border-slate-700">
<tr>
<th style={{ width: columnWidths.articleNo, ...(isPinned('articleNo') ? { left: getStickyLeft('articleNo') ?? 0, zIndex: getStickyRank('articleNo') ?? 0 } : {}) }} className={cn(
<th style={{ width: columnWidths.articleNo, ...(isPinned('articleNo') ? { left: getStickyLeft('articleNo') ?? 0, zIndex: getHeaderStickyRank('articleNo') ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned('articleNo') && "sticky bg-slate-900"
)} onClick={() => handleSort('articleNo')}>
@@ -1172,7 +1180,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
)}
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'articleNo', columnWidths.articleNo); }} />
</th>
<th style={{ width: columnWidths.articleName, ...(isPinned('articleName') ? { left: getStickyLeft('articleName') ?? 0, zIndex: getStickyRank('articleName') ?? 0 } : {}) }} className={cn(
<th style={{ width: columnWidths.articleName, ...(isPinned('articleName') ? { left: getStickyLeft('articleName') ?? 0, zIndex: getHeaderStickyRank('articleName') ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned('articleName') && "sticky bg-slate-900"
)} onClick={() => handleSort('articleName')}>
@@ -1200,7 +1208,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
)}
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'articleName', columnWidths.articleName); }} />
</th>
<th style={{ width: columnWidths.line, ...(isPinned('line') ? { left: getStickyLeft('line') ?? 0, zIndex: getStickyRank('line') ?? 0 } : {}) }} className={cn(
<th style={{ width: columnWidths.line, ...(isPinned('line') ? { left: getStickyLeft('line') ?? 0, zIndex: getHeaderStickyRank('line') ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned('line') && "sticky bg-slate-900"
)} onClick={() => handleSort('line')}>
@@ -1231,7 +1239,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
)}
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'line', columnWidths.line); }} />
</th>
<th style={{ width: columnWidths.classification, ...(isPinned('classification') ? { left: getStickyLeft('classification') ?? 0, zIndex: getStickyRank('classification') ?? 0 } : {}) }} className={cn(
<th style={{ width: columnWidths.classification, ...(isPinned('classification') ? { left: getStickyLeft('classification') ?? 0, zIndex: getHeaderStickyRank('classification') ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned('classification') && "sticky bg-slate-900"
)} onClick={() => handleSort('classification')}>
@@ -1263,7 +1271,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'classification', columnWidths.classification); }} />
</th>
<th style={{ width: columnWidths.productType, ...(isPinned('productType') ? { left: getStickyLeft('productType') ?? 0, zIndex: getStickyRank('productType') ?? 0 } : {}) }} className={cn(
<th style={{ width: columnWidths.productType, ...(isPinned('productType') ? { left: getStickyLeft('productType') ?? 0, zIndex: getHeaderStickyRank('productType') ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-purple-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned('productType') && "sticky bg-slate-900"
)} onClick={() => handleSort('productType')}>
@@ -1291,7 +1299,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'productType', columnWidths.productType); }} />
</th>
<th style={{ width: columnWidths.itemToLogistic, ...(isPinned('itemToLogistic') ? { left: getStickyLeft('itemToLogistic') ?? 0, zIndex: getStickyRank('itemToLogistic') ?? 0 } : {}) }} className={cn(
<th style={{ width: columnWidths.itemToLogistic, ...(isPinned('itemToLogistic') ? { left: getStickyLeft('itemToLogistic') ?? 0, zIndex: getHeaderStickyRank('itemToLogistic') ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-pink-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned('itemToLogistic') && "sticky bg-slate-900"
)} onClick={() => handleSort('itemToLogistic')}>
@@ -1305,7 +1313,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
const colKey = `prc_${col.index}`;
const width = columnWidths[colKey] ?? 100;
return (
<th key={col.index} style={{ width, ...(isPinned(colKey) ? { left: getStickyLeft(colKey) ?? 0, zIndex: getStickyRank(colKey) ?? 0 } : {}) }} className={cn(
<th key={col.index} style={{ width, ...(isPinned(colKey) ? { left: getStickyLeft(colKey) ?? 0, zIndex: getHeaderStickyRank(colKey) ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-blue-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned(colKey) && "sticky bg-slate-900"
)} onClick={() => handleSort(col.index)}>
@@ -1347,7 +1355,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
);
})}
<th style={{ width: columnWidths.unitsOuter, ...(isPinned('unitsOuter') ? { left: getStickyLeft('unitsOuter') ?? 0, zIndex: getStickyRank('unitsOuter') ?? 0 } : {}) }} className={cn(
<th style={{ width: columnWidths.unitsOuter, ...(isPinned('unitsOuter') ? { left: getStickyLeft('unitsOuter') ?? 0, zIndex: getHeaderStickyRank('unitsOuter') ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned('unitsOuter') && "sticky bg-slate-900"
)} onClick={() => handleSort('unitsOuter')}>
@@ -1379,7 +1387,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'unitsOuter', columnWidths.unitsOuter); }} />
</th>
<th style={{ width: columnWidths.outerW, ...(isPinned('outerW') ? { left: getStickyLeft('outerW') ?? 0, zIndex: getStickyRank('outerW') ?? 0 } : {}) }} className={cn(
<th style={{ width: columnWidths.outerW, ...(isPinned('outerW') ? { left: getStickyLeft('outerW') ?? 0, zIndex: getHeaderStickyRank('outerW') ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned('outerW') && "sticky bg-slate-900"
)} onClick={() => handleSort('outerW')}>
@@ -1411,7 +1419,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'outerW', columnWidths.outerW); }} />
</th>
<th style={{ width: columnWidths.outerL, ...(isPinned('outerL') ? { left: getStickyLeft('outerL') ?? 0, zIndex: getStickyRank('outerL') ?? 0 } : {}) }} className={cn(
<th style={{ width: columnWidths.outerL, ...(isPinned('outerL') ? { left: getStickyLeft('outerL') ?? 0, zIndex: getHeaderStickyRank('outerL') ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned('outerL') && "sticky bg-slate-900"
)} onClick={() => handleSort('outerL')}>
@@ -1479,7 +1487,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
const colKey = `con_${col.index}`;
const width = columnWidths[colKey] ?? 110;
return (
<th key={col.index} style={{ width, ...(isPinned(colKey) ? { left: getStickyLeft(colKey) ?? 0, zIndex: getStickyRank(colKey) ?? 0 } : {}) }} className={cn(
<th key={col.index} style={{ width, ...(isPinned(colKey) ? { left: getStickyLeft(colKey) ?? 0, zIndex: getHeaderStickyRank(colKey) ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-orange-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned(colKey) && "sticky bg-slate-900"
)} onClick={() => handleSort(col.index)}>
+9 -8
View File
@@ -4,6 +4,7 @@ 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';
interface ProductDescriptionsProps {
data: ExcelRow[];
@@ -19,15 +20,15 @@ export function ProductDescriptions({ data, headers, asinColumnIndex, onEdit, ro
// 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] = useState<TabType>('all');
const [search, setSearch] = useState('');
const [lineFilter, setLineFilter] = useState('');
const [licenseFilter, setLicenseFilter] = useState('');
const [sortCol, setSortCol] = useState<number | null>(null);
const [sortDesc, setSortDesc] = useState(false);
const [pageSize, setPageSize] = useState(100);
const [activeTab, setActiveTab] = usePersistentState<string>('descriptions-tab', 'all');
const [search, setSearch] = usePersistentState('global-search', '');
const [lineFilter, setLineFilter] = usePersistentState('descriptions-lineFilter', '');
const [licenseFilter, setLicenseFilter] = usePersistentState('descriptions-licenseFilter', '');
const [sortCol, setSortCol] = usePersistentState<number | null>('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] = useState<Record<number, string[]>>({});
const [columnFilters, setColumnFilters] = usePersistentState<Record<number, string[]>>('descriptions-columnFilters', {});
const [openFilterCol, setOpenFilterCol] = useState<number | null>(null);
const [columnWidths, setColumnWidths] = useState<Record<number, number>>({
[COLUMNS.ARTICLE_NO]: 110,
+40
View File
@@ -0,0 +1,40 @@
import React, { createContext, useContext, useState, useCallback } from 'react';
interface FilterContextType {
states: Record<string, any>;
setState: (key: string, value: any) => void;
}
const FilterContext = createContext<FilterContextType | undefined>(undefined);
export function FilterProvider({ children }: { children: React.ReactNode }) {
const [states, setStates] = useState<Record<string, any>>({});
const setState = useCallback((key: string, value: any) => {
setStates(prev => ({
...prev,
[key]: typeof value === 'function' ? value(prev[key]) : value
}));
}, []);
return (
<FilterContext.Provider value={{ states, setState }}>
{children}
</FilterContext.Provider>
);
}
export function usePersistentState<T>(key: string, defaultValue: T): [T, (value: T | ((prev: T) => T)) => void] {
const context = useContext(FilterContext);
if (!context) {
throw new Error('usePersistentState must be used within a FilterProvider');
}
const state = context.states[key] !== undefined ? context.states[key] : defaultValue;
const setState = useCallback((value: T | ((prev: T) => T)) => {
context.setState(key, value);
}, [context, key]);
return [state, setState];
}