mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 12:35:25 +02:00
Enable multi-select in Pricing search dropdown
Search dropdown now allows selecting multiple products with checkboxes and Apply button
This commit is contained in:
+110
-28
@@ -50,6 +50,7 @@ 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 [filterMode, setFilterMode] = useState<FilterMode>('all_errors');
|
const [filterMode, setFilterMode] = useState<FilterMode>('all_errors');
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
const [searchMode, setSearchMode] = useState<'all' | 'selected'>('all');
|
||||||
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);
|
||||||
@@ -64,6 +65,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
const [dynamicColFilters, setDynamicColFilters] = useState<Record<number, string[]>>({});
|
const [dynamicColFilters, setDynamicColFilters] = useState<Record<number, string[]>>({});
|
||||||
|
|
||||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||||
|
const [selectedSearchItems, setSelectedSearchItems] = useState<Set<number>>(new Set());
|
||||||
const searchDropdownRef = useRef<HTMLDivElement>(null);
|
const searchDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Close search dropdown on click outside
|
// Close search dropdown on click outside
|
||||||
@@ -78,13 +80,36 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const searchSuggestions = useMemo(() => {
|
const searchSuggestions = useMemo(() => {
|
||||||
if (!search) return data.slice(0, 50); // Show first 50 as default
|
if (!search && selectedSearchItems.size === 0) return data.slice(0, 50);
|
||||||
|
if (!search) return [];
|
||||||
const s = search.toLowerCase();
|
const s = search.toLowerCase();
|
||||||
return data.filter(r =>
|
return data.filter(r =>
|
||||||
String(r[COLUMNS.ARTICLE_NO] || '').toLowerCase().includes(s) ||
|
String(r[COLUMNS.ARTICLE_NO] || '').toLowerCase().includes(s) ||
|
||||||
String(r[COLUMNS.ARTICLE_NAME] || '').toLowerCase().includes(s)
|
String(r[COLUMNS.ARTICLE_NAME] || '').toLowerCase().includes(s)
|
||||||
).slice(0, 50);
|
).slice(0, 50);
|
||||||
}, [data, search]);
|
}, [data, search, selectedSearchItems]);
|
||||||
|
|
||||||
|
const handleSearchItemClick = (index: number) => {
|
||||||
|
const newSelected = new Set(selectedSearchItems);
|
||||||
|
if (newSelected.has(index)) {
|
||||||
|
newSelected.delete(index);
|
||||||
|
} else {
|
||||||
|
newSelected.add(index);
|
||||||
|
}
|
||||||
|
setSelectedSearchItems(newSelected);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApplySearch = () => {
|
||||||
|
if (selectedSearchItems.size > 0) {
|
||||||
|
setSearchMode('selected');
|
||||||
|
}
|
||||||
|
setIsSearchOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearSearch = () => {
|
||||||
|
setSelectedSearchItems(new Set());
|
||||||
|
setSearchMode('all');
|
||||||
|
};
|
||||||
|
|
||||||
// ── Dynamic column detection ──────────────────────────────────────────────
|
// ── Dynamic column detection ──────────────────────────────────────────────
|
||||||
const { uvpIdx, srpCols, containerCols } = useMemo(() => {
|
const { uvpIdx, srpCols, containerCols } = useMemo(() => {
|
||||||
@@ -211,6 +236,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Selected items from search dropdown
|
||||||
|
if (searchMode === 'selected' && selectedSearchItems.size > 0) {
|
||||||
|
result = result.filter(r => {
|
||||||
|
const articleNo = String(r.row[COLUMNS.ARTICLE_NO] || '');
|
||||||
|
return selectedSearchItems.has(articleNo);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Column filters
|
// Column filters
|
||||||
if (nameColFilter) {
|
if (nameColFilter) {
|
||||||
const s = nameColFilter.toLowerCase();
|
const s = nameColFilter.toLowerCase();
|
||||||
@@ -444,9 +477,18 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
onFocus={() => setIsSearchOpen(true)}
|
onFocus={() => setIsSearchOpen(true)}
|
||||||
className="w-full pl-4 pr-10 py-2 bg-slate-800 border border-slate-700 rounded-lg text-sm text-white focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-all"
|
className="w-full pl-4 pr-10 py-2 bg-slate-800 border border-slate-700 rounded-lg text-sm text-white focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-all"
|
||||||
/>
|
/>
|
||||||
{search ? (
|
{selectedSearchItems.size > 0 ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => { setSearch(''); setIsSearchOpen(false); }}
|
onClick={handleClearSearch}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-blue-400 hover:text-white flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<span className="text-xs bg-blue-600 text-white rounded-full w-5 h-5 flex items-center justify-center">
|
||||||
|
{selectedSearchItems.size}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
) : search ? (
|
||||||
|
<button
|
||||||
|
onClick={() => { setSearch(''); setSearchMode('all'); }}
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-white"
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-white"
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4" />
|
<X className="w-4 h-4" />
|
||||||
@@ -462,19 +504,54 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
{isSearchOpen && searchSuggestions.length > 0 && (
|
{isSearchOpen && searchSuggestions.length > 0 && (
|
||||||
<div className="absolute top-full left-0 right-0 mt-1 bg-slate-800 border border-slate-700 rounded-lg shadow-2xl z-[150] overflow-hidden animate-in fade-in slide-in-from-top-2 duration-200">
|
<div className="absolute top-full left-0 right-0 mt-1 bg-slate-800 border border-slate-700 rounded-lg shadow-2xl z-[150] overflow-hidden animate-in fade-in slide-in-from-top-2 duration-200">
|
||||||
<div className="max-h-64 overflow-y-auto">
|
<div className="max-h-64 overflow-y-auto">
|
||||||
{searchSuggestions.map((row, idx) => (
|
{searchSuggestions.map((row, idx) => {
|
||||||
<button
|
return (
|
||||||
key={`${row[COLUMNS.ARTICLE_NO]}-${idx}`}
|
<button
|
||||||
onClick={() => {
|
key={`${row[COLUMNS.ARTICLE_NO]}-${idx}`}
|
||||||
setSearch(String(row[COLUMNS.ARTICLE_NO]));
|
onClick={() => {
|
||||||
setIsSearchOpen(false);
|
const key = String(row[COLUMNS.ARTICLE_NO]);
|
||||||
}}
|
setSelectedSearchItems(prev => {
|
||||||
className="w-full text-left px-4 py-2.5 hover:bg-slate-700/50 flex flex-col gap-0.5 border-b border-slate-700/30 last:border-0 transition-colors"
|
const next = new Set(prev);
|
||||||
>
|
if (next.has(key)) next.delete(key);
|
||||||
<span className="text-xs font-mono font-bold text-blue-400">{row[COLUMNS.ARTICLE_NO]}</span>
|
else next.add(key);
|
||||||
<span className="text-xs text-slate-300 truncate">{row[COLUMNS.ARTICLE_NAME]}</span>
|
return next;
|
||||||
</button>
|
});
|
||||||
))}
|
}}
|
||||||
|
className="w-full text-left px-4 py-2.5 hover:bg-slate-700/50 flex items-center gap-3 border-b border-slate-700/30 last:border-0 transition-colors"
|
||||||
|
>
|
||||||
|
<div className={cn(
|
||||||
|
"w-4 h-4 rounded border flex items-center justify-center shrink-0",
|
||||||
|
selectedSearchItems.has(String(row[COLUMNS.ARTICLE_NO]))
|
||||||
|
? "bg-blue-600 border-blue-600"
|
||||||
|
: "border-slate-600"
|
||||||
|
)}>
|
||||||
|
{selectedSearchItems.has(String(row[COLUMNS.ARTICLE_NO])) && (
|
||||||
|
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<span className="text-xs font-mono font-bold text-blue-400">{row[COLUMNS.ARTICLE_NO]}</span>
|
||||||
|
<span className="text-xs text-slate-300 truncate">{row[COLUMNS.ARTICLE_NAME]}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between px-4 py-2 bg-slate-900/50 border-t border-slate-700">
|
||||||
|
<button
|
||||||
|
onClick={handleClearSearch}
|
||||||
|
className="text-xs text-slate-400 hover:text-white"
|
||||||
|
>
|
||||||
|
Clear ({selectedSearchItems.size})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleApplySearch}
|
||||||
|
className="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-xs rounded"
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{searchSuggestions.length === 50 && (
|
{searchSuggestions.length === 50 && (
|
||||||
<div className="px-4 py-1.5 bg-slate-900/50 text-[10px] text-slate-500 border-t border-slate-700 italic">
|
<div className="px-4 py-1.5 bg-slate-900/50 text-[10px] text-slate-500 border-t border-slate-700 italic">
|
||||||
@@ -813,15 +890,17 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
key={dataIndex}
|
key={dataIndex}
|
||||||
className={cn(
|
className={cn(
|
||||||
'border-b border-slate-700/50 transition-colors hover:bg-slate-700/20',
|
'border-b border-slate-700/50 transition-colors hover:bg-slate-700/20',
|
||||||
saveStatus === 'error'
|
isValidated
|
||||||
? 'bg-red-400/20 border-l-4 border-l-red-500'
|
? 'bg-emerald-500/5'
|
||||||
: saveStatus === 'pending'
|
: saveStatus === 'error'
|
||||||
? 'bg-yellow-400/20 border-l-4 border-l-yellow-400'
|
? 'bg-red-400/20 border-l-4 border-l-red-500'
|
||||||
: isCritical
|
: saveStatus === 'pending'
|
||||||
? 'bg-red-950/20'
|
? 'bg-yellow-400/20 border-l-4 border-l-yellow-400'
|
||||||
: pricingErrors.length > 0
|
: isCritical
|
||||||
? 'bg-amber-950/10'
|
? 'bg-red-950/20'
|
||||||
: ''
|
: pricingErrors.length > 0
|
||||||
|
? 'bg-amber-950/10'
|
||||||
|
: ''
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Art. No. */}
|
{/* Art. No. */}
|
||||||
@@ -953,8 +1032,11 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Actions sticky */}
|
<td className={cn(
|
||||||
<td className="px-2 py-2.5 sticky right-0 bg-slate-800 shadow-[-4px_0_8px_rgba(0,0,0,0.1)] group-hover:bg-slate-700/40">
|
"px-2 py-2.5 sticky right-0 shadow-[-4px_0_8px_rgba(0,0,0,0.1)] transition-colors",
|
||||||
|
isValidated ? "bg-[#1e2a25]" : "bg-slate-800",
|
||||||
|
"group-hover:bg-slate-700/40"
|
||||||
|
)}>
|
||||||
<button
|
<button
|
||||||
onClick={() => onEdit(dataIndex)}
|
onClick={() => onEdit(dataIndex)}
|
||||||
className="p-1.5 text-slate-500 hover:text-slate-200 hover:bg-slate-700 rounded transition-colors"
|
className="p-1.5 text-slate-500 hover:text-slate-200 hover:bg-slate-700 rounded transition-colors"
|
||||||
|
|||||||
Reference in New Issue
Block a user