mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 14:35:23 +02:00
UI: Add multi-select filter dropdown to Type column in Pricing view
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
f545776e15
commit
e7a3a4619d
@@ -15,6 +15,7 @@ import {
|
|||||||
Check,
|
Check,
|
||||||
Search,
|
Search,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
|
Maximize2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
import { ColumnFilterPopover } from './ColumnFilterPopover';
|
import { ColumnFilterPopover } from './ColumnFilterPopover';
|
||||||
@@ -78,6 +79,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [lineMultiFilter, setLineMultiFilter] = useState<string[]>([]);
|
const [lineMultiFilter, setLineMultiFilter] = useState<string[]>([]);
|
||||||
const [classificationFilter, setClassificationFilter] = useState<string[]>([]);
|
const [classificationFilter, setClassificationFilter] = useState<string[]>([]);
|
||||||
|
const [productTypeFilter, setProductTypeFilter] = useState<string[]>([]);
|
||||||
const [nameColFilter, setNameColFilter] = useState('');
|
const [nameColFilter, setNameColFilter] = useState('');
|
||||||
const [openFilter, setOpenFilter] = useState<string | null>(null);
|
const [openFilter, setOpenFilter] = useState<string | null>(null);
|
||||||
const [unitsOuterFilter, setUnitsOuterFilter] = useState<string[]>([]);
|
const [unitsOuterFilter, setUnitsOuterFilter] = useState<string[]>([]);
|
||||||
@@ -92,6 +94,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
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] = useState<{ key: string | number; direction: 'asc' | 'desc' | null }>({ key: null, direction: null });
|
||||||
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
|
||||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||||
const [selectedSearchItems, setSelectedSearchItems] = useState<Set<number>>(new Set());
|
const [selectedSearchItems, setSelectedSearchItems] = useState<Set<number>>(new Set());
|
||||||
@@ -276,6 +279,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
Array.from(new Set(data.map(r => String(r[COLUMNS.CLASSIFICATION] || '')))).sort(),
|
Array.from(new Set(data.map(r => String(r[COLUMNS.CLASSIFICATION] || '')))).sort(),
|
||||||
[data]);
|
[data]);
|
||||||
|
|
||||||
|
const uniqueProductTypes = useMemo(() =>
|
||||||
|
Array.from(new Set(data.map(r => String(r[COLUMNS.PRODUCT_TYPE] || '')).filter(Boolean))).sort(),
|
||||||
|
[data]);
|
||||||
|
|
||||||
const uniqueUnitsOuter = useMemo(() =>
|
const uniqueUnitsOuter = useMemo(() =>
|
||||||
Array.from(new Set(data.map(r => String(r[unitsOuterIdx] || '')))).filter(v => v).sort(),
|
Array.from(new Set(data.map(r => String(r[unitsOuterIdx] || '')))).filter(v => v).sort(),
|
||||||
[data, unitsOuterIdx]);
|
[data, unitsOuterIdx]);
|
||||||
@@ -331,6 +338,9 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
if (classificationFilter.length > 0) {
|
if (classificationFilter.length > 0) {
|
||||||
result = result.filter(r => classificationFilter.includes(String(r.row[COLUMNS.CLASSIFICATION] || '')));
|
result = result.filter(r => classificationFilter.includes(String(r.row[COLUMNS.CLASSIFICATION] || '')));
|
||||||
}
|
}
|
||||||
|
if (productTypeFilter.length > 0) {
|
||||||
|
result = result.filter(r => productTypeFilter.includes(String(r.row[COLUMNS.PRODUCT_TYPE] || '')));
|
||||||
|
}
|
||||||
if (unitsOuterFilter.length > 0) {
|
if (unitsOuterFilter.length > 0) {
|
||||||
result = result.filter(r => unitsOuterFilter.includes(String(r.row[unitsOuterIdx] || '')));
|
result = result.filter(r => unitsOuterFilter.includes(String(r.row[unitsOuterIdx] || '')));
|
||||||
}
|
}
|
||||||
@@ -361,7 +371,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, [analyzedRows, filterMode, search, nameColFilter, lineMultiFilter, classificationFilter, unitsOuterFilter, outerWFilter, outerLFilter, outerHFilter, dynamicColFilters, weightIssueFilter]);
|
}, [analyzedRows, filterMode, search, nameColFilter, lineMultiFilter, classificationFilter, productTypeFilter, unitsOuterFilter, outerWFilter, outerLFilter, outerHFilter, dynamicColFilters, weightIssueFilter]);
|
||||||
|
|
||||||
// ── Sorted rows ──────────────────────────────────────────────────────────
|
// ── Sorted rows ──────────────────────────────────────────────────────────
|
||||||
const sortedRows = useMemo(() => {
|
const sortedRows = useMemo(() => {
|
||||||
@@ -646,7 +656,16 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full gap-4 relative">
|
<div className={isFullscreen ? "fixed inset-0 z-40 bg-[#041021] p-4 overflow-auto" : "space-y-6"}>
|
||||||
|
{isFullscreen && (
|
||||||
|
<button
|
||||||
|
onClick={() => setIsFullscreen(false)}
|
||||||
|
className="fixed top-4 right-4 z-50 w-10 h-10 flex items-center justify-center bg-slate-800/90 backdrop-blur border border-slate-600 text-white rounded hover:bg-slate-700 hover:scale-105 transition-all"
|
||||||
|
title="Exit fullscreen"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{/* ── Anna Note Editor Modal ── */}
|
{/* ── Anna Note Editor Modal ── */}
|
||||||
{annaEditor && (
|
{annaEditor && (
|
||||||
<div className="fixed inset-0 bg-slate-950/50 backdrop-blur-sm z-[100] flex items-center justify-center p-4">
|
<div className="fixed inset-0 bg-slate-950/50 backdrop-blur-sm z-[100] flex items-center justify-center p-4">
|
||||||
@@ -857,6 +876,13 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
<StatCard label="Pricing Issues" value={stats.withPricing} icon={<DollarSign className="w-4 h-4" />} color="amber" />
|
<StatCard label="Pricing Issues" value={stats.withPricing} icon={<DollarSign className="w-4 h-4" />} color="amber" />
|
||||||
<StatCard label="Units Issues" value={stats.withUnits} icon={<AlertCircle className="w-4 h-4" />} color="red" />
|
<StatCard label="Units Issues" value={stats.withUnits} icon={<AlertCircle className="w-4 h-4" />} color="red" />
|
||||||
<StatCard label="All OK" value={stats.allOk} icon={<CheckCircle2 className="w-4 h-4" />} color="emerald" />
|
<StatCard label="All OK" value={stats.allOk} icon={<CheckCircle2 className="w-4 h-4" />} color="emerald" />
|
||||||
|
<button
|
||||||
|
onClick={() => setIsFullscreen(true)}
|
||||||
|
className="flex items-center justify-center w-full h-full min-h-[80px] bg-slate-800/60 hover:bg-slate-700 border border-slate-700/50 rounded-xl transition-colors"
|
||||||
|
title="Maximize view"
|
||||||
|
>
|
||||||
|
<Maximize2 className="w-5 h-5 text-slate-400" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Table ── */}
|
{/* ── Table ── */}
|
||||||
@@ -951,7 +977,25 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th style={{ width: columnWidths.productType }} className="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" onClick={() => handleSort('productType')}>
|
<th style={{ width: columnWidths.productType }} className="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" onClick={() => handleSort('productType')}>
|
||||||
<span className="flex items-center gap-1">Type <SortIcon current={sortConfig.key === 'productType' ? sortConfig.direction : null} /></span>
|
<span className="flex items-center gap-1">
|
||||||
|
Type <SortIcon current={sortConfig.key === 'productType' ? sortConfig.direction : null} />
|
||||||
|
<button
|
||||||
|
onClick={(e: React.MouseEvent) => { e.stopPropagation(); setOpenFilter(openFilter === 'productType' ? null : 'productType'); }}
|
||||||
|
className={cn('ml-1 p-0.5 rounded transition-colors', productTypeFilter.length > 0 ? 'text-purple-400 bg-purple-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100')}
|
||||||
|
>
|
||||||
|
<Filter className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
{openFilter === 'productType' && (
|
||||||
|
<ColumnFilterPopover
|
||||||
|
uniqueValues={uniqueProductTypes}
|
||||||
|
selectedValues={productTypeFilter}
|
||||||
|
onToggle={(val: string) => setProductTypeFilter((prev: string[]) => prev.includes(val) ? prev.filter((v: string) => v !== val) : [...prev, val])}
|
||||||
|
onSelectAll={vals => setProductTypeFilter(vals)}
|
||||||
|
onClear={() => { setProductTypeFilter([]); setOpenFilter(null); }}
|
||||||
|
onClose={() => setOpenFilter(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'productType', columnWidths.productType); }} />
|
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'productType', columnWidths.productType); }} />
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user