mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 15:45:24 +02:00
feat: persistent search and filters across all tabs via FilterContext
This commit is contained in:
@@ -4,6 +4,7 @@ import { X } from 'lucide-react';
|
|||||||
import { cn } from './lib/utils';
|
import { cn } from './lib/utils';
|
||||||
import { AppState, ExcelRow, resolveColumnIndices, COLUMNS } from './types';
|
import { AppState, ExcelRow, resolveColumnIndices, COLUMNS } from './types';
|
||||||
import { ColumnsProvider } from './contexts/ColumnsContext';
|
import { ColumnsProvider } from './contexts/ColumnsContext';
|
||||||
|
import { FilterProvider } from './contexts/FilterContext';
|
||||||
import { Sidebar } from './components/Sidebar';
|
import { Sidebar } from './components/Sidebar';
|
||||||
import { TopBar } from './components/TopBar';
|
import { TopBar } from './components/TopBar';
|
||||||
import { ProductDescriptions } from './components/ProductDescriptions';
|
import { ProductDescriptions } from './components/ProductDescriptions';
|
||||||
@@ -618,6 +619,7 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ColumnsProvider headers={appState.headers}>
|
<ColumnsProvider headers={appState.headers}>
|
||||||
|
<FilterProvider>
|
||||||
<div className="h-screen bg-[#040d1a] text-slate-200 flex flex-col font-sans overflow-hidden">
|
<div className="h-screen bg-[#040d1a] text-slate-200 flex flex-col font-sans overflow-hidden">
|
||||||
{!isMaximized && (
|
{!isMaximized && (
|
||||||
<TopBar
|
<TopBar
|
||||||
@@ -803,6 +805,7 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</FilterProvider>
|
||||||
</ColumnsProvider>
|
</ColumnsProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, Edit2, Package,
|
|||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
import { ConfirmModal } from './ConfirmModal';
|
import { ConfirmModal } from './ConfirmModal';
|
||||||
import { ColumnFilterPopover } from './ColumnFilterPopover';
|
import { ColumnFilterPopover } from './ColumnFilterPopover';
|
||||||
|
import { usePersistentState } from '../contexts/FilterContext';
|
||||||
|
|
||||||
interface DimensionsViewProps {
|
interface DimensionsViewProps {
|
||||||
data: ExcelRow[];
|
data: ExcelRow[];
|
||||||
@@ -53,9 +54,9 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat
|
|||||||
targetGroupKey: string;
|
targetGroupKey: string;
|
||||||
selectedIndices: number[];
|
selectedIndices: number[];
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = usePersistentState('global-search', '');
|
||||||
const [lineFilter, setLineFilter] = useState<string[]>([]);
|
const [lineFilter, setLineFilter] = usePersistentState<string[]>('dimensions-lineFilter', []);
|
||||||
const [classFilter, setClassFilter] = useState<string[]>([]);
|
const [classFilter, setClassFilter] = usePersistentState<string[]>('dimensions-classFilter', []);
|
||||||
const [openFilter, setOpenFilter] = useState<'line' | 'class' | null>(null);
|
const [openFilter, setOpenFilter] = useState<'line' | 'class' | null>(null);
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [dragStart, setDragStart] = useState<number | null>(null);
|
const [dragStart, setDragStart] = useState<number | null>(null);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { getHistory, deleteHistoryEntry, HistoryEntry } from '../lib/supabase';
|
|||||||
import { ExcelRow } from '../types';
|
import { ExcelRow } from '../types';
|
||||||
import { useColumns } from '../contexts/ColumnsContext';
|
import { useColumns } from '../contexts/ColumnsContext';
|
||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
|
import { usePersistentState } from '../contexts/FilterContext';
|
||||||
|
|
||||||
interface HistoryViewProps {
|
interface HistoryViewProps {
|
||||||
headers: string[];
|
headers: string[];
|
||||||
@@ -17,7 +18,7 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp
|
|||||||
const [history, setHistory] = useState<HistoryEntry[]>([]);
|
const [history, setHistory] = useState<HistoryEntry[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = usePersistentState('global-search', '');
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ExcelRow } from '../types';
|
|||||||
import { Search, Filter, ChevronDown, ChevronUp, X, Maximize2 } from 'lucide-react';
|
import { Search, Filter, ChevronDown, ChevronUp, X, Maximize2 } from 'lucide-react';
|
||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
import { ColumnFilterPopover } from './ColumnFilterPopover';
|
import { ColumnFilterPopover } from './ColumnFilterPopover';
|
||||||
|
import { usePersistentState } from '../contexts/FilterContext';
|
||||||
|
|
||||||
interface MatrixViewProps {
|
interface MatrixViewProps {
|
||||||
data: ExcelRow[];
|
data: ExcelRow[];
|
||||||
@@ -13,11 +14,11 @@ interface MatrixViewProps {
|
|||||||
export function MatrixView({ data, headers, rowStatuses }: MatrixViewProps) {
|
export function MatrixView({ data, headers, rowStatuses }: MatrixViewProps) {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(25);
|
const [pageSize, setPageSize] = useState(25);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = usePersistentState('global-search', '');
|
||||||
const [columnFilters, setColumnFilters] = useState<Record<number, string[]>>({});
|
const [columnFilters, setColumnFilters] = usePersistentState<Record<number, string[]>>('matrix-columnFilters', {});
|
||||||
const [openFilterCol, setOpenFilterCol] = useState<number | null>(null);
|
const [openFilterCol, setOpenFilterCol] = useState<number | null>(null);
|
||||||
const [sortCol, setSortCol] = useState<number | null>(null);
|
const [sortCol, setSortCol] = usePersistentState<number | null>('matrix-sortCol', null);
|
||||||
const [sortDesc, setSortDesc] = useState(false);
|
const [sortDesc, setSortDesc] = usePersistentState('matrix-sortDesc', false);
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Search, ChevronDown, ChevronUp, X, Edit2, Save, Maximize2 } from 'lucid
|
|||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
import { ColumnFilterPopover } from './ColumnFilterPopover';
|
import { ColumnFilterPopover } from './ColumnFilterPopover';
|
||||||
import { DateFilterPopover } from './DateFilterPopover';
|
import { DateFilterPopover } from './DateFilterPopover';
|
||||||
|
import { usePersistentState } from '../contexts/FilterContext';
|
||||||
|
|
||||||
interface MissingDataViewProps {
|
interface MissingDataViewProps {
|
||||||
data: ExcelRow[];
|
data: ExcelRow[];
|
||||||
@@ -67,14 +68,14 @@ interface EditingState {
|
|||||||
|
|
||||||
export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: MissingDataViewProps) {
|
export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: MissingDataViewProps) {
|
||||||
const COLUMNS = useColumns();
|
const COLUMNS = useColumns();
|
||||||
const [activeTab, setActiveTab] = useState<TabType>('missingLaunch');
|
const [activeTab, setActiveTab] = usePersistentState<TabType>('missingData-tab', 'missingLaunch');
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = usePersistentState('global-search', '');
|
||||||
const [sortCol, setSortCol] = useState<number | null>(null);
|
const [sortCol, setSortCol] = usePersistentState<number | null>('missingData-sortCol', null);
|
||||||
const [sortDesc, setSortDesc] = useState(false);
|
const [sortDesc, setSortDesc] = usePersistentState('missingData-sortDesc', false);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [editing, setEditing] = useState<EditingState | null>(null);
|
const [editing, setEditing] = useState<EditingState | null>(null);
|
||||||
const [columnFilters, setColumnFilters] = useState<Record<number, string[]>>({});
|
const [columnFilters, setColumnFilters] = usePersistentState<Record<number, string[]>>('missingData-columnFilters', {});
|
||||||
const [dateFilters, setDateFilters] = useState<Record<number, { start: string; end: string }>>({});
|
const [dateFilters, setDateFilters] = usePersistentState<Record<number, { start: string; end: string }>>('missingData-dateFilters', {});
|
||||||
const [openFilter, setOpenFilter] = useState<number | null>(null);
|
const [openFilter, setOpenFilter] = useState<number | null>(null);
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
const pageSize = 100;
|
const pageSize = 100;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ExcelRow } from '../types';
|
|||||||
import { useColumns } from '../contexts/ColumnsContext';
|
import { useColumns } from '../contexts/ColumnsContext';
|
||||||
import { Clock, Undo2, Package, Box, DollarSign, FileText, Search, Filter, X, Maximize2 } from 'lucide-react';
|
import { Clock, Undo2, Package, Box, DollarSign, FileText, Search, Filter, X, Maximize2 } from 'lucide-react';
|
||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
|
import { usePersistentState } from '../contexts/FilterContext';
|
||||||
|
|
||||||
interface PendingValidationViewProps {
|
interface PendingValidationViewProps {
|
||||||
data: ExcelRow[];
|
data: ExcelRow[];
|
||||||
@@ -14,7 +15,7 @@ interface PendingValidationViewProps {
|
|||||||
|
|
||||||
export function PendingValidationView({ data, pendingRows, rowStatuses, onRevertRow, onEdit }: PendingValidationViewProps) {
|
export function PendingValidationView({ data, pendingRows, rowStatuses, onRevertRow, onEdit }: PendingValidationViewProps) {
|
||||||
const COLUMNS = useColumns();
|
const COLUMNS = useColumns();
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = usePersistentState('global-search', '');
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
|
||||||
const pendingEntries = Object.entries(pendingRows);
|
const pendingEntries = Object.entries(pendingRows);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
import { ColumnFilterPopover } from './ColumnFilterPopover';
|
import { ColumnFilterPopover } from './ColumnFilterPopover';
|
||||||
|
import { usePersistentState } from '../contexts/FilterContext';
|
||||||
|
|
||||||
interface PricingViewProps {
|
interface PricingViewProps {
|
||||||
data: ExcelRow[];
|
data: ExcelRow[];
|
||||||
@@ -53,8 +54,8 @@ function findCol(headers: string[], ...keywords: string[]): number {
|
|||||||
|
|
||||||
export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, rowStatuses }: PricingViewProps) {
|
export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, rowStatuses }: PricingViewProps) {
|
||||||
const COLUMNS = useColumns();
|
const COLUMNS = useColumns();
|
||||||
const [filterMode, setFilterMode] = useState<FilterMode>('all_errors');
|
const [filterMode, setFilterMode] = usePersistentState<FilterMode>('pricing-filterMode', 'all_errors');
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = usePersistentState('global-search', '');
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const pageSize = 100;
|
const pageSize = 100;
|
||||||
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({
|
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 [editingCell, setEditingCell] = useState<EditingCell | null>(null);
|
||||||
const [savingCell, setSavingCell] = useState<{ rowIndex: number; colIndex: number } | null>(null);
|
const [savingCell, setSavingCell] = useState<{ rowIndex: number; colIndex: number } | null>(null);
|
||||||
const inputRef = useRef<HTMLInputElement>(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 [editingType, setEditingType] = useState<{ rowIndex: number; value: string } | null>(null);
|
||||||
const [editingLogistic, setEditingLogistic] = useState<{ rowIndex: number; value: string } | null>(null);
|
const [editingLogistic, setEditingLogistic] = useState<{ rowIndex: number; value: string } | null>(null);
|
||||||
const [typeSuggestions, setTypeSuggestions] = useState<string[]>([]);
|
const [typeSuggestions, setTypeSuggestions] = useState<string[]>([]);
|
||||||
const typeInputRef = useRef<HTMLInputElement>(null);
|
const typeInputRef = useRef<HTMLInputElement>(null);
|
||||||
const logisticInputRef = 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 [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 [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||||
const [selectedSearchItems, setSelectedSearchItems] = useState<Set<string>>(new Set());
|
|
||||||
const searchDropdownRef = useRef<HTMLDivElement>(null);
|
const searchDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Pinned columns state
|
// 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);
|
const [showPinPanel, setShowPinPanel] = useState(false);
|
||||||
|
|
||||||
// Close search dropdown on click outside
|
// Close search dropdown on click outside
|
||||||
@@ -641,6 +642,13 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
};
|
};
|
||||||
}, [pinnedColumns, pricingEditableCols, containerCols]);
|
}, [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);
|
const isPinned = (key: string) => pinnedColumns.has(key);
|
||||||
|
|
||||||
// ═ Unique values for dynamic pricing columns ═════════════════════════════
|
// ═ 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">
|
<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">
|
<thead className="sticky top-0 z-10 bg-slate-900 border-b border-slate-700">
|
||||||
<tr>
|
<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",
|
"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"
|
isPinned('articleNo') && "sticky bg-slate-900"
|
||||||
)} onClick={() => handleSort('articleNo')}>
|
)} 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); }} />
|
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'articleNo', columnWidths.articleNo); }} />
|
||||||
</th>
|
</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",
|
"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"
|
isPinned('articleName') && "sticky bg-slate-900"
|
||||||
)} onClick={() => handleSort('articleName')}>
|
)} 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); }} />
|
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'articleName', columnWidths.articleName); }} />
|
||||||
</th>
|
</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",
|
"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"
|
isPinned('line') && "sticky bg-slate-900"
|
||||||
)} onClick={() => handleSort('line')}>
|
)} 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); }} />
|
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'line', columnWidths.line); }} />
|
||||||
</th>
|
</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",
|
"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"
|
isPinned('classification') && "sticky bg-slate-900"
|
||||||
)} onClick={() => handleSort('classification')}>
|
)} 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); }} />
|
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'classification', columnWidths.classification); }} />
|
||||||
</th>
|
</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",
|
"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"
|
isPinned('productType') && "sticky bg-slate-900"
|
||||||
)} onClick={() => handleSort('productType')}>
|
)} 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); }} />
|
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'productType', columnWidths.productType); }} />
|
||||||
</th>
|
</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",
|
"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"
|
isPinned('itemToLogistic') && "sticky bg-slate-900"
|
||||||
)} onClick={() => handleSort('itemToLogistic')}>
|
)} onClick={() => handleSort('itemToLogistic')}>
|
||||||
@@ -1305,7 +1313,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
const colKey = `prc_${col.index}`;
|
const colKey = `prc_${col.index}`;
|
||||||
const width = columnWidths[colKey] ?? 100;
|
const width = columnWidths[colKey] ?? 100;
|
||||||
return (
|
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",
|
"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"
|
isPinned(colKey) && "sticky bg-slate-900"
|
||||||
)} onClick={() => handleSort(col.index)}>
|
)} 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",
|
"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"
|
isPinned('unitsOuter') && "sticky bg-slate-900"
|
||||||
)} onClick={() => handleSort('unitsOuter')}>
|
)} 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); }} />
|
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'unitsOuter', columnWidths.unitsOuter); }} />
|
||||||
</th>
|
</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",
|
"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"
|
isPinned('outerW') && "sticky bg-slate-900"
|
||||||
)} onClick={() => handleSort('outerW')}>
|
)} 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); }} />
|
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'outerW', columnWidths.outerW); }} />
|
||||||
</th>
|
</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",
|
"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"
|
isPinned('outerL') && "sticky bg-slate-900"
|
||||||
)} onClick={() => handleSort('outerL')}>
|
)} onClick={() => handleSort('outerL')}>
|
||||||
@@ -1479,7 +1487,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
const colKey = `con_${col.index}`;
|
const colKey = `con_${col.index}`;
|
||||||
const width = columnWidths[colKey] ?? 110;
|
const width = columnWidths[colKey] ?? 110;
|
||||||
return (
|
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",
|
"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"
|
isPinned(colKey) && "sticky bg-slate-900"
|
||||||
)} onClick={() => handleSort(col.index)}>
|
)} onClick={() => handleSort(col.index)}>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useColumns } from '../contexts/ColumnsContext';
|
|||||||
import { Search, Filter, Edit2, ChevronDown, ChevronUp, X, Maximize2 } from 'lucide-react';
|
import { Search, Filter, Edit2, ChevronDown, ChevronUp, X, Maximize2 } from 'lucide-react';
|
||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
import { ColumnFilterPopover } from './ColumnFilterPopover';
|
import { ColumnFilterPopover } from './ColumnFilterPopover';
|
||||||
|
import { usePersistentState } from '../contexts/FilterContext';
|
||||||
|
|
||||||
interface ProductDescriptionsProps {
|
interface ProductDescriptionsProps {
|
||||||
data: ExcelRow[];
|
data: ExcelRow[];
|
||||||
@@ -19,15 +20,15 @@ export function ProductDescriptions({ data, headers, asinColumnIndex, onEdit, ro
|
|||||||
|
|
||||||
// Description columns that should only have Present/Missing filters
|
// 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 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 [activeTab, setActiveTab] = usePersistentState<string>('descriptions-tab', 'all');
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = usePersistentState('global-search', '');
|
||||||
const [lineFilter, setLineFilter] = useState('');
|
const [lineFilter, setLineFilter] = usePersistentState('descriptions-lineFilter', '');
|
||||||
const [licenseFilter, setLicenseFilter] = useState('');
|
const [licenseFilter, setLicenseFilter] = usePersistentState('descriptions-licenseFilter', '');
|
||||||
const [sortCol, setSortCol] = useState<number | null>(null);
|
const [sortCol, setSortCol] = usePersistentState<number | null>('descriptions-sortCol', null);
|
||||||
const [sortDesc, setSortDesc] = useState(false);
|
const [sortDesc, setSortDesc] = usePersistentState('descriptions-sortDesc', false);
|
||||||
const [pageSize, setPageSize] = useState(100);
|
const [pageSize, setPageSize] = usePersistentState('descriptions-pageSize', 100);
|
||||||
const [page, setPage] = useState(1);
|
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 [openFilterCol, setOpenFilterCol] = useState<number | null>(null);
|
||||||
const [columnWidths, setColumnWidths] = useState<Record<number, number>>({
|
const [columnWidths, setColumnWidths] = useState<Record<number, number>>({
|
||||||
[COLUMNS.ARTICLE_NO]: 110,
|
[COLUMNS.ARTICLE_NO]: 110,
|
||||||
|
|||||||
@@ -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];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user