feat: save/load/delete filters directly in FilterPanel dropdown
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useRef, useEffect, useMemo } from 'react';
|
||||
import { Filter, Plus, X, ChevronDown } from 'lucide-react';
|
||||
import { Filter, Plus, X, ChevronDown, Bookmark } from 'lucide-react';
|
||||
import type { UnifiedContact } from '@/api/unifiedContacts';
|
||||
|
||||
// ─── Field definitions ───────────────────────────────────────────────────────
|
||||
@@ -215,10 +215,20 @@ function getOrderedFields(contactType?: 'company' | 'person' | undefined): Filte
|
||||
|
||||
// ─── Component ────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface SavedFilter {
|
||||
id: string;
|
||||
name: string;
|
||||
filterState: FilterState;
|
||||
}
|
||||
|
||||
interface FilterPanelProps {
|
||||
filters: FilterState;
|
||||
onFiltersChange: (filters: FilterState) => void;
|
||||
contactType?: 'company' | 'person' | undefined;
|
||||
savedFilters?: SavedFilter[];
|
||||
onSaveFilter?: (name: string, filterState: FilterState) => void;
|
||||
onLoadFilter?: (filterState: FilterState) => void;
|
||||
onDeleteFilter?: (id: string) => void;
|
||||
}
|
||||
|
||||
let condIdCounter = 0;
|
||||
@@ -226,7 +236,7 @@ function newConditionId() {
|
||||
return `cond-${Date.now()}-${++condIdCounter}`;
|
||||
}
|
||||
|
||||
export function FilterPanel({ filters, onFiltersChange, contactType }: FilterPanelProps) {
|
||||
export function FilterPanel({ filters, onFiltersChange, contactType, savedFilters = [], onSaveFilter, onLoadFilter, onDeleteFilter }: FilterPanelProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const btnRef = useRef<HTMLDivElement>(null);
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
@@ -290,6 +300,14 @@ export function FilterPanel({ filters, onFiltersChange, contactType }: FilterPan
|
||||
onFiltersChange({ ...filters, logic: filters.logic === 'AND' ? 'OR' : 'AND' });
|
||||
};
|
||||
|
||||
const handleSaveFilter = () => {
|
||||
if (!onSaveFilter) return;
|
||||
if (filters.conditions.length === 0) return;
|
||||
const name = window.prompt('Name für diesen Filter:', 'Mein Filter');
|
||||
if (!name) return;
|
||||
onSaveFilter(name, { ...filters, conditions: filters.conditions.map((c) => ({ ...c })) });
|
||||
};
|
||||
|
||||
// Group fields by group for the dropdown
|
||||
const groupedFields = useMemo(() => {
|
||||
const groups: Record<string, FilterFieldDef[]> = {};
|
||||
@@ -423,6 +441,32 @@ export function FilterPanel({ filters, onFiltersChange, contactType }: FilterPan
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Saved filters */}
|
||||
{savedFilters.length > 0 && (
|
||||
<div className="px-4 py-2 border-b border-secondary-100">
|
||||
<div className="text-[10px] font-semibold uppercase tracking-wide text-secondary-400 mb-1.5">Gespeicherte Filter</div>
|
||||
<div className="space-y-0.5">
|
||||
{savedFilters.map((sf) => (
|
||||
<div key={sf.id} className="flex items-center gap-1 group">
|
||||
<button
|
||||
onClick={() => onLoadFilter?.(sf.filterState)}
|
||||
className="flex-1 flex items-center gap-1.5 px-2 py-1 text-xs text-left rounded text-secondary-700 hover:bg-secondary-100 transition-colors"
|
||||
>
|
||||
<Bookmark className="w-3 h-3 text-primary-500 flex-shrink-0" />
|
||||
<span className="truncate">{sf.name}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onDeleteFilter?.(sf.id)}
|
||||
className="opacity-0 group-hover:opacity-100 p-1 rounded text-secondary-400 hover:text-red-600 transition-opacity"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Filter rows */}
|
||||
<div className="px-4 py-3 space-y-2">
|
||||
{filters.conditions.length === 0 && (
|
||||
@@ -526,13 +570,24 @@ export function FilterPanel({ filters, onFiltersChange, contactType }: FilterPan
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-t border-secondary-100">
|
||||
<button
|
||||
onClick={addCondition}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
Bedingung hinzufügen
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={addCondition}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
Bedingung hinzufügen
|
||||
</button>
|
||||
{activeCount > 0 && onSaveFilter && (
|
||||
<button
|
||||
onClick={handleSaveFilter}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors"
|
||||
>
|
||||
<Bookmark className="w-3.5 h-3.5" />
|
||||
Filter speichern
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
className="px-3 py-1 text-xs font-medium text-white bg-primary-600 hover:bg-primary-700 rounded transition-colors"
|
||||
|
||||
Reference in New Issue
Block a user