mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-17 16:03:41 +00:00
feat: add saved filter presets for case list
Users can save frequently used filter combinations (year, fallgruppe, ICD status, search) as named presets. Stored server-side in new filter_presets table (max 10 per user). Star-icon Popover on CasesPage to load, save, or delete presets. TanStack Query hooks for CRUD. New files: FilterPreset model, migration 009, API router, schemas, useFilterPresets hook. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d5db84d93f
commit
f3846813a4
9 changed files with 371 additions and 2 deletions
40
backend/alembic/versions/009_filter_presets.py
Normal file
40
backend/alembic/versions/009_filter_presets.py
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
"""Add filter_presets table.
|
||||||
|
|
||||||
|
Revision ID: 009_filter_presets
|
||||||
|
Revises: 008_password_reset_tokens
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
revision = "009_filter_presets"
|
||||||
|
down_revision = "008_password_reset_tokens"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
"filter_presets",
|
||||||
|
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||||
|
sa.Column(
|
||||||
|
"user_id",
|
||||||
|
sa.Integer,
|
||||||
|
sa.ForeignKey("users.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column("name", sa.String(100), nullable=False),
|
||||||
|
sa.Column("filters", sa.String(1000), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"created_at",
|
||||||
|
sa.DateTime,
|
||||||
|
nullable=False,
|
||||||
|
server_default=sa.func.now(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
op.create_index("idx_fp_user", "filter_presets", ["user_id"])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index("idx_fp_user", table_name="filter_presets")
|
||||||
|
op.drop_table("filter_presets")
|
||||||
95
backend/app/api/filter_presets.py
Normal file
95
backend/app/api/filter_presets.py
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
"""Filter presets API — save/load/delete user-specific filter combinations."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.dependencies import get_current_user
|
||||||
|
from app.database import get_db
|
||||||
|
from app.models.user import FilterPreset, User
|
||||||
|
from app.schemas.filter_preset import FilterPresetCreate, FilterPresetResponse, FilterValues
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
MAX_PRESETS_PER_USER = 10
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=list[FilterPresetResponse])
|
||||||
|
def list_presets(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Return all filter presets for the current user."""
|
||||||
|
presets = (
|
||||||
|
db.query(FilterPreset)
|
||||||
|
.filter(FilterPreset.user_id == user.id)
|
||||||
|
.order_by(FilterPreset.created_at.desc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
result = []
|
||||||
|
for p in presets:
|
||||||
|
filters = json.loads(p.filters) if isinstance(p.filters, str) else p.filters
|
||||||
|
result.append(FilterPresetResponse(
|
||||||
|
id=p.id,
|
||||||
|
name=p.name,
|
||||||
|
filters=FilterValues(**filters),
|
||||||
|
created_at=p.created_at,
|
||||||
|
))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=FilterPresetResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
def create_preset(
|
||||||
|
data: FilterPresetCreate,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Create a new filter preset for the current user."""
|
||||||
|
count = db.query(FilterPreset).filter(FilterPreset.user_id == user.id).count()
|
||||||
|
if count >= MAX_PRESETS_PER_USER:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=f"Maximal {MAX_PRESETS_PER_USER} gespeicherte Filter erlaubt.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not data.name.strip():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
|
detail="Name darf nicht leer sein.",
|
||||||
|
)
|
||||||
|
|
||||||
|
preset = FilterPreset(
|
||||||
|
user_id=user.id,
|
||||||
|
name=data.name.strip()[:100],
|
||||||
|
filters=json.dumps(data.filters.model_dump(exclude_none=True)),
|
||||||
|
)
|
||||||
|
db.add(preset)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(preset)
|
||||||
|
|
||||||
|
filters = json.loads(preset.filters)
|
||||||
|
return FilterPresetResponse(
|
||||||
|
id=preset.id,
|
||||||
|
name=preset.name,
|
||||||
|
filters=FilterValues(**filters),
|
||||||
|
created_at=preset.created_at,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{preset_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
def delete_preset(
|
||||||
|
preset_id: int,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Delete a filter preset (own presets only)."""
|
||||||
|
preset = (
|
||||||
|
db.query(FilterPreset)
|
||||||
|
.filter(FilterPreset.id == preset_id, FilterPreset.user_id == user.id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not preset:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Preset nicht gefunden.")
|
||||||
|
db.delete(preset)
|
||||||
|
db.commit()
|
||||||
|
|
@ -12,6 +12,7 @@ from app.api.cases import router as cases_router
|
||||||
from app.api.coding import router as coding_router
|
from app.api.coding import router as coding_router
|
||||||
from app.api.import_router import router as import_router
|
from app.api.import_router import router as import_router
|
||||||
from app.api.notifications import router as notifications_router
|
from app.api.notifications import router as notifications_router
|
||||||
|
from app.api.filter_presets import router as filter_presets_router
|
||||||
from app.api.reports import router as reports_router
|
from app.api.reports import router as reports_router
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
|
|
||||||
|
|
@ -35,6 +36,7 @@ app.include_router(cases_router, prefix="/api/cases", tags=["cases"])
|
||||||
app.include_router(notifications_router, prefix="/api/notifications", tags=["notifications"])
|
app.include_router(notifications_router, prefix="/api/notifications", tags=["notifications"])
|
||||||
app.include_router(coding_router, prefix="/api/coding", tags=["coding"])
|
app.include_router(coding_router, prefix="/api/coding", tags=["coding"])
|
||||||
app.include_router(reports_router, prefix="/api/reports", tags=["reports"])
|
app.include_router(reports_router, prefix="/api/reports", tags=["reports"])
|
||||||
|
app.include_router(filter_presets_router, prefix="/api/filter-presets", tags=["filter-presets"])
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/health")
|
@app.get("/api/health")
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,28 @@ class PasswordResetToken(Base):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FilterPreset(Base):
|
||||||
|
__tablename__ = "filter_presets"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
user_id: Mapped[int] = mapped_column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("users.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||||
|
filters: Mapped[str] = mapped_column(String(1000), nullable=False)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime, nullable=False, server_default=func.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
user: Mapped[User] = relationship()
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
Index("idx_fp_user", "user_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AllowedDomain(Base):
|
class AllowedDomain(Base):
|
||||||
__tablename__ = "allowed_domains"
|
__tablename__ = "allowed_domains"
|
||||||
|
|
||||||
|
|
|
||||||
33
backend/app/schemas/filter_preset.py
Normal file
33
backend/app/schemas/filter_preset.py
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
"""Pydantic schemas for filter presets."""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class FilterValues(BaseModel):
|
||||||
|
"""The actual filter values stored in a preset."""
|
||||||
|
|
||||||
|
jahr: Optional[int] = None
|
||||||
|
fallgruppe: Optional[str] = None
|
||||||
|
has_icd: Optional[str] = None
|
||||||
|
search: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class FilterPresetCreate(BaseModel):
|
||||||
|
"""Request body for creating a filter preset."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
filters: FilterValues
|
||||||
|
|
||||||
|
|
||||||
|
class FilterPresetResponse(BaseModel):
|
||||||
|
"""Response for a filter preset."""
|
||||||
|
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
filters: FilterValues
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
model_config = {"from_attributes": True}
|
||||||
44
docs/plans/2026-02-28-filter-presets-design.md
Normal file
44
docs/plans/2026-02-28-filter-presets-design.md
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# Erweiterte Suche mit Filterspeicherung — Design
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Nutzer können häufig genutzte Filter-Kombinationen als Presets speichern und per Klick wiederherstellen.
|
||||||
|
|
||||||
|
## Backend
|
||||||
|
|
||||||
|
### Datenmodell: `filter_presets`
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|------|-----|-------------|
|
||||||
|
| id | int PK | Auto-increment |
|
||||||
|
| user_id | int FK | users.id |
|
||||||
|
| name | varchar(100) | Anzeigename |
|
||||||
|
| filters | JSON | `{jahr, fallgruppe, has_icd, search}` |
|
||||||
|
| created_at | datetime | server_default=now() |
|
||||||
|
|
||||||
|
Max 10 Presets pro User (enforced im Endpoint).
|
||||||
|
|
||||||
|
### Endpoints
|
||||||
|
|
||||||
|
- `GET /filter-presets` — Alle Presets des Users
|
||||||
|
- `POST /filter-presets` — Neues Preset (Name + Filter-JSON)
|
||||||
|
- `DELETE /filter-presets/{id}` — Preset löschen (nur eigene)
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
Dropdown-Button "Gespeicherte Filter" neben dem Download-Button auf der CasesPage.
|
||||||
|
- Klick auf Preset → Filter anwenden
|
||||||
|
- "Aktuellen Filter speichern" → Name-Dialog
|
||||||
|
- X-Icon zum Löschen
|
||||||
|
|
||||||
|
## Dateien
|
||||||
|
|
||||||
|
| Datei | Änderung |
|
||||||
|
|-------|----------|
|
||||||
|
| `backend/app/models/user.py` | `FilterPreset` Modell |
|
||||||
|
| `backend/alembic/versions/009_filter_presets.py` | Migration |
|
||||||
|
| `backend/app/schemas/filter_preset.py` | Pydantic Schemas |
|
||||||
|
| `backend/app/api/filter_presets.py` | Neue API-Datei |
|
||||||
|
| `backend/app/main.py` | Router registrieren |
|
||||||
|
| `frontend/src/hooks/useFilterPresets.ts` | TanStack Query Hook |
|
||||||
|
| `frontend/src/pages/CasesPage.tsx` | Preset-Dropdown |
|
||||||
|
|
@ -15,6 +15,6 @@
|
||||||
|
|
||||||
## Niedrige Priorität
|
## Niedrige Priorität
|
||||||
|
|
||||||
- [ ] **Erweiterte Suche mit Filterspeicherung** — Häufig genutzte Filter als Presets speichern (z.B. "Onko ohne ICD 2026").
|
- [x] **Erweiterte Suche mit Filterspeicherung** — ✅ Implementiert: Filter-Presets pro User in DB gespeichert (max 10). Popover-Dropdown mit Stern-Icon auf CasesPage. Klick lädt Filter, X löscht Preset, "+" speichert aktuellen Filter.
|
||||||
- [ ] **Dashboard: Durchlaufzeiten** — Durchschnittliche Dauer von Fallerfassung bis Gutachten visualisieren.
|
- [ ] **Dashboard: Durchlaufzeiten** — Durchschnittliche Dauer von Fallerfassung bis Gutachten visualisieren.
|
||||||
- [x] **Passwort-Reset per E-Mail** — ✅ Implementiert: "Passwort vergessen?" auf Login-Seite, Reset-Link per E-Mail (1h gültig), ResetPasswordPage mit Passwort-Formular. Rate-Limiting (3/h), Audit-Log, Token als SHA-256 Hash gespeichert.
|
- [x] **Passwort-Reset per E-Mail** — ✅ Implementiert: "Passwort vergessen?" auf Login-Seite, Reset-Link per E-Mail (1h gültig), ResetPasswordPage mit Passwort-Formular. Rate-Limiting (3/h), Audit-Log, Token als SHA-256 Hash gespeichert.
|
||||||
|
|
|
||||||
40
frontend/src/hooks/useFilterPresets.ts
Normal file
40
frontend/src/hooks/useFilterPresets.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import api from '@/services/api'
|
||||||
|
|
||||||
|
export interface FilterValues {
|
||||||
|
jahr?: number
|
||||||
|
fallgruppe?: string
|
||||||
|
has_icd?: string
|
||||||
|
search?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterPreset {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
filters: FilterValues
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFilterPresets() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['filter-presets'],
|
||||||
|
queryFn: () => api.get<FilterPreset[]>('/filter-presets/').then(r => r.data),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateFilterPreset() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data: { name: string; filters: FilterValues }) =>
|
||||||
|
api.post<FilterPreset>('/filter-presets/', data).then(r => r.data),
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['filter-presets'] }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteFilterPreset() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: number) => api.delete(`/filter-presets/${id}`),
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['filter-presets'] }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||||
import type { KeyboardEvent } from 'react'
|
import type { KeyboardEvent } from 'react'
|
||||||
import {
|
import {
|
||||||
Search, ChevronLeft, ChevronRight, Save, CheckCircle, XCircle, Pencil, X, Info, Loader2, Download,
|
Search, ChevronLeft, ChevronRight, Save, CheckCircle, XCircle, Pencil, X, Info, Loader2, Download, Star, Trash2,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import type { Case } from '@/types'
|
import type { Case } from '@/types'
|
||||||
import { useAuth } from '@/context/AuthContext'
|
import { useAuth } from '@/context/AuthContext'
|
||||||
import { useCases, usePendingIcdCases, useIcdUpdate, type CaseFilters } from '@/hooks/useCases'
|
import { useCases, usePendingIcdCases, useIcdUpdate, type CaseFilters } from '@/hooks/useCases'
|
||||||
import api from '@/services/api'
|
import api from '@/services/api'
|
||||||
|
import { useFilterPresets, useCreateFilterPreset, useDeleteFilterPreset } from '@/hooks/useFilterPresets'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
|
@ -22,6 +23,9 @@ import {
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
|
import {
|
||||||
|
Popover, PopoverContent, PopoverTrigger,
|
||||||
|
} from '@/components/ui/popover'
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
import {
|
import {
|
||||||
Tooltip, TooltipContent, TooltipTrigger,
|
Tooltip, TooltipContent, TooltipTrigger,
|
||||||
|
|
@ -73,6 +77,12 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) {
|
||||||
const [sheetOpen, setSheetOpen] = useState(false)
|
const [sheetOpen, setSheetOpen] = useState(false)
|
||||||
const [batchMode, setBatchMode] = useState(false)
|
const [batchMode, setBatchMode] = useState(false)
|
||||||
const [exporting, setExporting] = useState(false)
|
const [exporting, setExporting] = useState(false)
|
||||||
|
const [presetOpen, setPresetOpen] = useState(false)
|
||||||
|
const [presetName, setPresetName] = useState('')
|
||||||
|
const [showSavePreset, setShowSavePreset] = useState(false)
|
||||||
|
const { data: presets } = useFilterPresets()
|
||||||
|
const createPreset = useCreateFilterPreset()
|
||||||
|
const deletePreset = useDeleteFilterPreset()
|
||||||
|
|
||||||
// Debounce search
|
// Debounce search
|
||||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
@ -142,6 +152,28 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const applyPreset = (filters: { jahr?: number; fallgruppe?: string; has_icd?: string; search?: string }) => {
|
||||||
|
setJahr(filters.jahr != null ? String(filters.jahr) : '__all__')
|
||||||
|
setFallgruppe(filters.fallgruppe || '__all__')
|
||||||
|
setHasIcd(filters.has_icd || '__all__')
|
||||||
|
setSearch(filters.search || '')
|
||||||
|
setDebouncedSearch(filters.search || '')
|
||||||
|
setPage(1)
|
||||||
|
setPresetOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const savePreset = async () => {
|
||||||
|
if (!presetName.trim()) return
|
||||||
|
const filterValues: Record<string, string | number> = {}
|
||||||
|
if (jahr !== '__all__') filterValues.jahr = Number(jahr)
|
||||||
|
if (fallgruppe !== '__all__') filterValues.fallgruppe = fallgruppe
|
||||||
|
if (hasIcd !== '__all__') filterValues.has_icd = hasIcd
|
||||||
|
if (debouncedSearch) filterValues.search = debouncedSearch
|
||||||
|
await createPreset.mutateAsync({ name: presetName.trim(), filters: filterValues })
|
||||||
|
setPresetName('')
|
||||||
|
setShowSavePreset(false)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-4">
|
<div className="p-6 space-y-4">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
|
|
@ -216,6 +248,67 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) {
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>Gefilterte Fallliste als Excel exportieren</TooltipContent>
|
<TooltipContent>Gefilterte Fallliste als Excel exportieren</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Popover open={presetOpen} onOpenChange={setPresetOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm" className="gap-1.5">
|
||||||
|
<Star className="size-4" />
|
||||||
|
Filter
|
||||||
|
{presets && presets.length > 0 && (
|
||||||
|
<Badge variant="secondary" className="ml-1 px-1.5 py-0 text-xs">{presets.length}</Badge>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-72 p-2" align="end">
|
||||||
|
<div className="space-y-1">
|
||||||
|
{presets && presets.length > 0 ? (
|
||||||
|
presets.map((p) => (
|
||||||
|
<div key={p.id} className="flex items-center gap-1 group">
|
||||||
|
<button
|
||||||
|
className="flex-1 text-left text-sm px-2 py-1.5 rounded hover:bg-accent transition-colors truncate"
|
||||||
|
onClick={() => applyPreset(p.filters)}
|
||||||
|
>
|
||||||
|
{p.name}
|
||||||
|
</button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-7 opacity-0 group-hover:opacity-100 transition-opacity shrink-0"
|
||||||
|
onClick={() => deletePreset.mutate(p.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-3.5 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground px-2 py-1.5">Keine gespeicherten Filter.</p>
|
||||||
|
)}
|
||||||
|
<div className="border-t pt-1 mt-1">
|
||||||
|
{showSavePreset ? (
|
||||||
|
<div className="flex items-center gap-1.5 px-1">
|
||||||
|
<Input
|
||||||
|
value={presetName}
|
||||||
|
onChange={(e) => setPresetName(e.target.value)}
|
||||||
|
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); savePreset() } if (e.key === 'Escape') setShowSavePreset(false) }}
|
||||||
|
placeholder="z.B. Onko ohne ICD"
|
||||||
|
className="h-8 text-sm"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<Button size="sm" className="h-8 px-2" onClick={savePreset} disabled={!presetName.trim() || createPreset.isPending}>
|
||||||
|
<Save className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="w-full text-left text-sm px-2 py-1.5 rounded hover:bg-accent transition-colors text-muted-foreground"
|
||||||
|
onClick={() => setShowSavePreset(true)}
|
||||||
|
>
|
||||||
|
+ Aktuellen Filter speichern
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue