feat: standardize Weight Issues filter to use ColumnFilterPopover

This commit is contained in:
Christian Vidal Wolf
2026-04-28 09:34:12 +02:00
parent 8e1a533ea2
commit 1c68e35ec4
2 changed files with 56 additions and 54 deletions
+56 -40
View File
@@ -105,7 +105,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
const [checkYingFilter, setCheckYingFilter] = usePersistentState<string[]>('pricing-checkYingFilter', []);
const [checkAnnaFilter, setCheckAnnaFilter] = usePersistentState<string[]>('pricing-checkAnnaFilter', []);
const [dynamicColFilters, setDynamicColFilters] = usePersistentState<Record<number, string[]>>('pricing-dynamicColFilters', {});
const [weightIssueFilter, setWeightIssueFilter] = usePersistentState<'all' | 'with' | 'without'>('pricing-weightIssueFilter', 'all');
const [weightIssueFilter, setWeightIssueFilter] = usePersistentState<string[]>('pricing-weightIssueFilter', []);
const [selectedSearchItems, setSelectedSearchItems] = usePersistentState<Set<string>>('pricing-selectedSearchItems', new Set());
const [openFilter, setOpenFilter] = useState<string | null>(null);
const [isSearchOpen, setIsSearchOpen] = useState(false);
@@ -115,15 +115,12 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
const [pinnedColumns, setPinnedColumns] = usePersistentState<Set<string>>('pricing-pinnedColumns', new Set(['articleNo', 'articleName']));
const [showPinPanel, setShowPinPanel] = useState(false);
// Drag-to-check state (Excel-style drag to toggle Check Ying)
// Drag-to-select state (Excel-style row selection)
const [dragSelectedRows, setDragSelectedRows] = useState<Set<number>>(new Set());
const isDraggingRef = useRef(false);
const dragStartIndexRef = useRef<number | null>(null);
const dragCheckValueRef = useRef<boolean>(true); // what value to set on mouseup
const lastDragIndexRef = useRef<number | null>(null);
const visibleDataIndexesRef = useRef<number[]>([]);
const dataRef = useRef(data);
dataRef.current = data;
const handleRowMouseDown = useCallback((e: React.MouseEvent, dataIndex: number) => {
if ((e.target as HTMLElement).closest('input, button, a')) return;
@@ -131,10 +128,8 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
isDraggingRef.current = true;
dragStartIndexRef.current = dataIndex;
lastDragIndexRef.current = dataIndex;
// Toggle based on current state of the first row touched
dragCheckValueRef.current = !dataRef.current[dataIndex]?.[COLUMNS.VALIDATED_CHECK];
setDragSelectedRows(new Set([dataIndex]));
}, [COLUMNS.VALIDATED_CHECK]);
}, []);
const handleRowMouseEnter = useCallback((dataIndex: number) => {
if (!isDraggingRef.current || dragStartIndexRef.current === null) return;
@@ -155,19 +150,18 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
}, []);
useEffect(() => {
const handleMouseUp = async () => {
if (!isDraggingRef.current) return;
isDraggingRef.current = false;
const rows = new Set(dragSelectedRows);
if (rows.size === 0) return;
setDragSelectedRows(new Set());
const targetValue = dragCheckValueRef.current;
for (const idx of Array.from(rows)) {
await handleToggleCheck(idx as number, targetValue);
}
};
const handleMouseUp = () => { isDraggingRef.current = false; };
document.addEventListener('mouseup', handleMouseUp);
return () => document.removeEventListener('mouseup', handleMouseUp);
}, []);
const applyCheckToSelection = useCallback(async (column: 'ying' | 'anna', value: boolean) => {
const rows = Array.from(dragSelectedRows) as number[];
setDragSelectedRows(new Set());
for (const idx of rows) {
if (column === 'ying') await handleToggleCheck(idx, value);
else await handleToggleAnnaCheck(idx, value);
}
}, [dragSelectedRows]);
// Close search dropdown on click outside
@@ -499,10 +493,13 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
});
// Weight issue filter
if (weightIssueFilter === 'with') {
result = result.filter(r => r.unitErrors.some(e => e.startsWith('NW')));
} else if (weightIssueFilter === 'without') {
result = result.filter(r => !r.unitErrors.some(e => e.startsWith('NW')));
if ((weightIssueFilter || []).length > 0) {
result = result.filter(r => {
const hasWeightError = r.unitErrors.some(e => e.startsWith('NW'));
if (weightIssueFilter.includes('with') && hasWeightError) return true;
if (weightIssueFilter.includes('without') && !hasWeightError) return true;
return false;
});
}
return result;
@@ -1792,30 +1789,27 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<div className="flex items-center gap-1">
<span>Weight Issues</span>
<button
onClick={() => setOpenFilter(openFilter === 'weightIssue' ? null : 'weightIssue')}
id="filter-trigger-weightIssue"
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'weightIssue' ? null : 'weightIssue'); }}
className={cn(
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
weightIssueFilter !== 'all' ? 'text-red-400 bg-red-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn',
(weightIssueFilter || []).length > 0 ? 'text-red-400 bg-red-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
)}
>
<Filter className="w-3 h-3" />
</button>
</div>
{openFilter === 'weightIssue' && (
<div className="absolute top-full left-0 mt-1 w-44 bg-slate-800 border border-slate-700 rounded-lg shadow-2xl z-50 p-1 flex flex-col gap-0.5 animate-in fade-in zoom-in-95 duration-100">
{(['all', 'with', 'without'] as const).map(opt => (
<button
key={opt}
onClick={() => { setWeightIssueFilter(opt); setOpenFilter(null); }}
className={cn(
'w-full text-left px-3 py-1.5 rounded text-xs transition-colors',
weightIssueFilter === opt ? 'bg-red-500/20 text-red-400' : 'text-slate-300 hover:bg-slate-700'
)}
>
{opt === 'all' ? 'All' : opt === 'with' ? 'With weight issue' : 'No weight issue'}
</button>
))}
</div>
<ColumnFilterPopover
triggerId="filter-trigger-weightIssue"
uniqueValues={['with', 'without']}
selectedValues={weightIssueFilter || []}
onToggle={val => setWeightIssueFilter(prev => (prev || []).includes(val) ? (prev || []).filter(v => v !== val) : [...(prev || []), val])}
onSelectAll={vals => setWeightIssueFilter(vals || [])}
onClear={() => { setWeightIssueFilter([]); setOpenFilter(null); }}
onClose={() => setOpenFilter(null)}
zIndex={50}
/>
)}
</th>
@@ -1833,6 +1827,17 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<Filter className="w-3 h-3" />
</button>
</div>
{dragSelectedRows.size > 0 && (
<div className="flex items-center gap-1 mt-1" onClick={(e: React.MouseEvent) => e.stopPropagation()}>
<input
type="checkbox"
title={`Marcar Check Ying en ${dragSelectedRows.size} filas`}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => applyCheckToSelection('ying', e.target.checked)}
className="w-3.5 h-3.5 rounded border-blue-500 bg-slate-900 text-blue-600 cursor-pointer"
/>
<span className="text-[10px] text-blue-300 whitespace-nowrap">{dragSelectedRows.size} filas</span>
</div>
)}
{openFilter === 'check' && (
<ColumnFilterPopover
triggerId="filter-trigger-check"
@@ -1862,6 +1867,17 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<Filter className="w-3 h-3" />
</button>
</div>
{dragSelectedRows.size > 0 && (
<div className="flex items-center gap-1 mt-1" onClick={(e: React.MouseEvent) => e.stopPropagation()}>
<input
type="checkbox"
title={`Marcar Check Anna en ${dragSelectedRows.size} filas`}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => applyCheckToSelection('anna', e.target.checked)}
className="w-3.5 h-3.5 rounded border-pink-500 bg-slate-900 text-pink-600 cursor-pointer"
/>
<span className="text-[10px] text-pink-300 whitespace-nowrap">{dragSelectedRows.size} filas</span>
</div>
)}
{openFilter === 'checkAnna' && (
<ColumnFilterPopover
triggerId="filter-trigger-checkAnna"