mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 13:35:25 +02:00
feat: persistent search and filters across all tabs via FilterContext
This commit is contained in:
@@ -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)}>
|
||||
|
||||
Reference in New Issue
Block a user