mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-17 17:13:42 +00:00
feat: add Excel export for filtered case list
New GET /cases/export endpoint generates .xlsx with openpyxl using the same filters as the case list (year, fallgruppe, ICD status, search). Role-aware columns: admins see patient names, DAK staff does not. Frontend adds a Download button next to the filter bar with loading state. Refactors shared query logic into _build_case_query helper. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
eb39346f02
commit
002021d7c7
4 changed files with 232 additions and 31 deletions
|
|
@ -1,11 +1,14 @@
|
||||||
"""Cases API routes — list, detail, update, ICD entry, coding, template download."""
|
"""Cases API routes — list, detail, update, ICD entry, coding, template download."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timezone
|
from datetime import date, datetime, timezone
|
||||||
|
from io import BytesIO
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
|
from openpyxl import Workbook
|
||||||
|
from openpyxl.styles import Font, PatternFill
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
|
@ -166,6 +169,136 @@ def download_coding_template(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Excel export
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
EXPORT_HEADER_FILL = PatternFill(start_color="FFD9D9D9", end_color="FFD9D9D9", fill_type="solid")
|
||||||
|
EXPORT_HEADER_FONT = Font(bold=True, name="Calibri", size=11)
|
||||||
|
|
||||||
|
FALLGRUPPEN_LABELS = {
|
||||||
|
"onko": "Onkologie",
|
||||||
|
"kardio": "Kardiologie",
|
||||||
|
"intensiv": "Intensivmedizin",
|
||||||
|
"galle": "Gallenblase",
|
||||||
|
"sd": "Schilddrüse",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_case_query(db: Session, user: User, *, jahr=None, fallgruppe=None, has_icd=None, search=None):
|
||||||
|
"""Build a filtered Case query (shared by list and export)."""
|
||||||
|
query = db.query(Case).filter(Case.versicherung == settings.VERSICHERUNG_FILTER)
|
||||||
|
if jahr is not None:
|
||||||
|
query = query.filter(Case.jahr == jahr)
|
||||||
|
if fallgruppe is not None:
|
||||||
|
query = query.filter(Case.fallgruppe == fallgruppe)
|
||||||
|
if has_icd is True:
|
||||||
|
query = query.filter(Case.icd != None) # noqa: E711
|
||||||
|
elif has_icd is False:
|
||||||
|
query = query.filter(Case.icd == None) # noqa: E711
|
||||||
|
if search:
|
||||||
|
like_pattern = f"%{search}%"
|
||||||
|
if user.role == "admin":
|
||||||
|
query = query.filter(
|
||||||
|
or_(Case.nachname.ilike(like_pattern), Case.vorname.ilike(like_pattern),
|
||||||
|
Case.fall_id.ilike(like_pattern), Case.kvnr.ilike(like_pattern))
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
query = query.filter(
|
||||||
|
or_(Case.fall_id.ilike(like_pattern), Case.kvnr.ilike(like_pattern))
|
||||||
|
)
|
||||||
|
return query.order_by(Case.datum.desc(), Case.id.desc())
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/export")
|
||||||
|
def export_cases(
|
||||||
|
request: Request,
|
||||||
|
jahr: Optional[int] = Query(None),
|
||||||
|
fallgruppe: Optional[str] = Query(None),
|
||||||
|
has_icd: Optional[bool] = Query(None),
|
||||||
|
search: Optional[str] = Query(None, min_length=1, max_length=200),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Export filtered cases as .xlsx download."""
|
||||||
|
query = _build_case_query(db, user, jahr=jahr, fallgruppe=fallgruppe, has_icd=has_icd, search=search)
|
||||||
|
cases = query.all()
|
||||||
|
|
||||||
|
is_admin = user.role == "admin"
|
||||||
|
|
||||||
|
# Define columns based on role
|
||||||
|
if is_admin:
|
||||||
|
columns = [
|
||||||
|
("Fall-ID", lambda c: c.fall_id or ""),
|
||||||
|
("Datum", lambda c: c.datum.strftime("%d.%m.%Y") if isinstance(c.datum, date) else str(c.datum or "")),
|
||||||
|
("Nachname", lambda c: c.nachname or ""),
|
||||||
|
("Vorname", lambda c: c.vorname or ""),
|
||||||
|
("KVNR", lambda c: c.kvnr or ""),
|
||||||
|
("Fallgruppe", lambda c: FALLGRUPPEN_LABELS.get(c.fallgruppe, c.fallgruppe or "")),
|
||||||
|
("ICD", lambda c: c.icd or ""),
|
||||||
|
("Gutachten-Typ", lambda c: c.gutachten_typ or ""),
|
||||||
|
("Therapieänderung", lambda c: c.therapieaenderung or ""),
|
||||||
|
("Unterlagen", lambda c: "Ja" if c.unterlagen else "Nein"),
|
||||||
|
("Gutachten", lambda c: "Ja" if c.gutachten else "Nein"),
|
||||||
|
("Abgelehnt", lambda c: "Ja" if c.ablehnung else "Nein"),
|
||||||
|
("Abgerechnet", lambda c: "Ja" if c.abgerechnet else "Nein"),
|
||||||
|
("Abbruch", lambda c: "Ja" if c.abbruch else "Nein"),
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
columns = [
|
||||||
|
("Fall-ID", lambda c: c.fall_id or ""),
|
||||||
|
("Datum", lambda c: c.datum.strftime("%d.%m.%Y") if isinstance(c.datum, date) else str(c.datum or "")),
|
||||||
|
("KVNR", lambda c: c.kvnr or ""),
|
||||||
|
("Fallgruppe", lambda c: FALLGRUPPEN_LABELS.get(c.fallgruppe, c.fallgruppe or "")),
|
||||||
|
("ICD", lambda c: c.icd or ""),
|
||||||
|
("Gutachten-Typ", lambda c: c.gutachten_typ or ""),
|
||||||
|
("Therapieänderung", lambda c: c.therapieaenderung or ""),
|
||||||
|
("Unterlagen", lambda c: "Ja" if c.unterlagen else "Nein"),
|
||||||
|
("Gutachten", lambda c: "Ja" if c.gutachten else "Nein"),
|
||||||
|
("Abgelehnt", lambda c: "Ja" if c.ablehnung else "Nein"),
|
||||||
|
("Abgerechnet", lambda c: "Ja" if c.abgerechnet else "Nein"),
|
||||||
|
("Abbruch", lambda c: "Ja" if c.abbruch else "Nein"),
|
||||||
|
]
|
||||||
|
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "Fallliste"
|
||||||
|
|
||||||
|
# Header row
|
||||||
|
for col_idx, (header, _) in enumerate(columns, 1):
|
||||||
|
cell = ws.cell(row=1, column=col_idx, value=header)
|
||||||
|
cell.font = EXPORT_HEADER_FONT
|
||||||
|
cell.fill = EXPORT_HEADER_FILL
|
||||||
|
|
||||||
|
# Data rows
|
||||||
|
for row_idx, case in enumerate(cases, 2):
|
||||||
|
for col_idx, (_, getter) in enumerate(columns, 1):
|
||||||
|
ws.cell(row=row_idx, column=col_idx, value=getter(case))
|
||||||
|
|
||||||
|
# Auto-width columns
|
||||||
|
for col_idx, (header, _) in enumerate(columns, 1):
|
||||||
|
from openpyxl.utils import get_column_letter
|
||||||
|
col_letter = get_column_letter(col_idx)
|
||||||
|
max_len = len(header)
|
||||||
|
for row in ws.iter_rows(min_row=2, min_col=col_idx, max_col=col_idx):
|
||||||
|
for cell in row:
|
||||||
|
if cell.value:
|
||||||
|
max_len = max(max_len, len(str(cell.value)))
|
||||||
|
ws.column_dimensions[col_letter].width = min(max_len + 2, 40)
|
||||||
|
|
||||||
|
buf = BytesIO()
|
||||||
|
wb.save(buf)
|
||||||
|
buf.seek(0)
|
||||||
|
|
||||||
|
filename = f"Fallliste_{date.today().strftime('%Y-%m-%d')}.xlsx"
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
buf,
|
||||||
|
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Disclosure request
|
# Disclosure request
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -314,40 +447,14 @@ def list_cases(
|
||||||
- ``has_coding``: True = only with gutachten_typ; False = only without
|
- ``has_coding``: True = only with gutachten_typ; False = only without
|
||||||
- ``search``: free-text search across nachname, vorname, fall_id, kvnr
|
- ``search``: free-text search across nachname, vorname, fall_id, kvnr
|
||||||
"""
|
"""
|
||||||
query = db.query(Case).filter(Case.versicherung == settings.VERSICHERUNG_FILTER)
|
query = _build_case_query(db, user, jahr=jahr, fallgruppe=fallgruppe, has_icd=has_icd, search=search)
|
||||||
|
|
||||||
if jahr is not None:
|
|
||||||
query = query.filter(Case.jahr == jahr)
|
|
||||||
if kw is not None:
|
if kw is not None:
|
||||||
query = query.filter(Case.kw == kw)
|
query = query.filter(Case.kw == kw)
|
||||||
if fallgruppe is not None:
|
|
||||||
query = query.filter(Case.fallgruppe == fallgruppe)
|
|
||||||
if has_icd is True:
|
|
||||||
query = query.filter(Case.icd != None) # noqa: E711
|
|
||||||
elif has_icd is False:
|
|
||||||
query = query.filter(Case.icd == None) # noqa: E711
|
|
||||||
if has_coding is True:
|
if has_coding is True:
|
||||||
query = query.filter(Case.gutachten_typ != None) # noqa: E711
|
query = query.filter(Case.gutachten_typ != None) # noqa: E711
|
||||||
elif has_coding is False:
|
elif has_coding is False:
|
||||||
query = query.filter(Case.gutachten_typ == None) # noqa: E711
|
query = query.filter(Case.gutachten_typ == None) # noqa: E711
|
||||||
if search:
|
|
||||||
like_pattern = f"%{search}%"
|
|
||||||
if user.role == "admin":
|
|
||||||
query = query.filter(
|
|
||||||
or_(
|
|
||||||
Case.nachname.ilike(like_pattern),
|
|
||||||
Case.vorname.ilike(like_pattern),
|
|
||||||
Case.fall_id.ilike(like_pattern),
|
|
||||||
Case.kvnr.ilike(like_pattern),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
query = query.filter(
|
|
||||||
or_(
|
|
||||||
Case.fall_id.ilike(like_pattern),
|
|
||||||
Case.kvnr.ilike(like_pattern),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
total = query.count()
|
total = query.count()
|
||||||
cases = (
|
cases = (
|
||||||
|
|
|
||||||
56
docs/plans/2026-02-28-excel-export-design.md
Normal file
56
docs/plans/2026-02-28-excel-export-design.md
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# Fallliste als Excel exportieren — Design
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Export-Button auf der Cases-Seite, der alle gefilterten Fälle als `.xlsx` herunterlädt. Nutzt aktive Filter (Jahr, Fallgruppe, ICD-Status, Suchbegriff).
|
||||||
|
|
||||||
|
## Architektur
|
||||||
|
|
||||||
|
**Backend-Endpoint** `GET /cases/export` generiert XLSX on-demand mit `openpyxl` (bereits installiert). Gleiche Filter-Parameter wie `GET /cases/`, aber ohne Pagination. Rollenbasierte Spaltenauswahl + Masking für DAK-Mitarbeiter.
|
||||||
|
|
||||||
|
**Frontend** ruft den Endpoint als Blob ab und triggert Browser-Download. Bewährtes Pattern aus ReportsPage.
|
||||||
|
|
||||||
|
## Backend
|
||||||
|
|
||||||
|
### Endpoint: `GET /cases/export`
|
||||||
|
|
||||||
|
- **Datei:** `backend/app/api/cases.py`
|
||||||
|
- **Query-Parameter:** `jahr`, `fallgruppe`, `has_icd`, `search` (optional)
|
||||||
|
- **Auth:** Alle authentifizierten Nutzer
|
||||||
|
- **Response:** `StreamingResponse` mit `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`
|
||||||
|
- **Dateiname:** `Fallliste_YYYY-MM-DD.xlsx`
|
||||||
|
|
||||||
|
### Spalten
|
||||||
|
|
||||||
|
**Admin:**
|
||||||
|
Fall-ID, Datum, Nachname, Vorname, KVNR, Fallgruppe, ICD, Gutachten-Typ, Therapieänderung, Unterlagen, Gutachten, Abgelehnt, Abgerechnet, Abbruch
|
||||||
|
|
||||||
|
**DAK-Mitarbeiter:**
|
||||||
|
Fall-ID, Datum, KVNR, Fallgruppe, ICD, Gutachten-Typ, Therapieänderung, Unterlagen, Gutachten, Abgelehnt, Abgerechnet, Abbruch
|
||||||
|
|
||||||
|
### Excel-Formatierung
|
||||||
|
|
||||||
|
- Header-Zeile: fett, hellgrauer Hintergrund
|
||||||
|
- Auto-Width für Spalten
|
||||||
|
- Ja/Nein für Boolean-Felder
|
||||||
|
- Datum als deutsches Format (DD.MM.YYYY)
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
### Button-Platzierung
|
||||||
|
|
||||||
|
Download-Icon-Button neben den bestehenden Filtern auf CasesPage. Sichtbar für alle Rollen.
|
||||||
|
|
||||||
|
### Download-Flow
|
||||||
|
|
||||||
|
1. Klick → Button zeigt Spinner
|
||||||
|
2. `api.get('/cases/export', { params, responseType: 'blob' })`
|
||||||
|
3. Blob → `URL.createObjectURL` → `<a download>` → Cleanup
|
||||||
|
4. Button kehrt zum Normalzustand zurück
|
||||||
|
|
||||||
|
## Dateien
|
||||||
|
|
||||||
|
| Datei | Änderung |
|
||||||
|
|-------|----------|
|
||||||
|
| `backend/app/api/cases.py` | Neuer `GET /cases/export` Endpoint |
|
||||||
|
| `frontend/src/pages/CasesPage.tsx` | Download-Button + Blob-Download-Logik |
|
||||||
|
|
@ -3,14 +3,14 @@
|
||||||
## Hohe Priorität
|
## Hohe Priorität
|
||||||
|
|
||||||
- [x] **Gutachten-Statistik Seite** — ✅ Implementiert: 4 KPIs, Stacked-Bar (Typ pro KW), Donut (Typ-Verteilung), Grouped-Bar (Therapieänderungen pro KW), Horizontal-Bars (Gründe). Backend-Endpoint + Frontend komplett.
|
- [x] **Gutachten-Statistik Seite** — ✅ Implementiert: 4 KPIs, Stacked-Bar (Typ pro KW), Donut (Typ-Verteilung), Grouped-Bar (Therapieänderungen pro KW), Horizontal-Bars (Gründe). Backend-Endpoint + Frontend komplett.
|
||||||
- [ ] **Fallliste als Excel exportieren** — Export-Button auf der Cases-Seite für gefilterte Falllisten als .xlsx Download. Nutzt aktive Filter (Jahr, Fallgruppe, ICD-Status).
|
- [x] **Fallliste als Excel exportieren** — ✅ Implementiert: Download-Button auf der Cases-Seite. Backend `GET /cases/export` generiert XLSX mit openpyxl. Rollenbasierte Spalten (Admin sieht Namen, DAK-Mitarbeiter nicht). Alle aktiven Filter werden berücksichtigt.
|
||||||
- [x] **E-Mail-Benachrichtigungen bei Freigabe-Entscheidung** — ✅ Implementiert: disclosure_service nutzt nun notification_service für In-App + E-Mail. Admins erhalten E-Mail bei neuer Anfrage, Mitarbeiter bei Genehmigung/Ablehnung.
|
- [x] **E-Mail-Benachrichtigungen bei Freigabe-Entscheidung** — ✅ Implementiert: disclosure_service nutzt nun notification_service für In-App + E-Mail. Admins erhalten E-Mail bei neuer Anfrage, Mitarbeiter bei Genehmigung/Ablehnung.
|
||||||
|
|
||||||
## Mittlere Priorität
|
## Mittlere Priorität
|
||||||
|
|
||||||
- [x] **Benachrichtigungs-Center (Bell-Icon)** — ✅ Bereits implementiert: Bell-Icon im Header mit Badge-Counter, Popover-Dropdown, Mark-as-read, 60s-Polling. War schon in Header.tsx vorhanden.
|
- [x] **Benachrichtigungs-Center (Bell-Icon)** — ✅ Bereits implementiert: Bell-Icon im Header mit Badge-Counter, Popover-Dropdown, Mark-as-read, 60s-Polling. War schon in Header.tsx vorhanden.
|
||||||
- [x] **Dashboard: Vorjahresvergleich bei KPIs** — ✅ Implementiert: prev_kpis im Dashboard-Endpoint, KpiCards zeigen farbige Trend-Indikatoren (+X% grün, -X% rot) mit Vorjahresvergleich.
|
- [x] **Dashboard: Vorjahresvergleich bei KPIs** — ✅ Implementiert: prev_kpis im Dashboard-Endpoint, KpiCards zeigen farbige Trend-Indikatoren (+X% grün, -X% rot) mit Vorjahresvergleich.
|
||||||
- [ ] **Batch-ICD-Eingabe** — Inline-Tabelle auf der ICD-Seite mit direkter ICD-Eingabe pro Zeile statt Einzelklick auf jeden Fall.
|
- [x] **Batch-ICD-Eingabe** — ✅ Implementiert: Switch-Toggle für Batch-Modus auf ICD-Seite. Inline-Eingabefelder mit Enter/Tab/Escape-Bedienung, visuelles Feedback (Spinner, Häkchen, Fehler-Tooltip).
|
||||||
- [x] **Dark Mode Toggle** — ✅ Bereits implementiert: Sun/Moon-Toggle im Header, useTheme Hook aktiv.
|
- [x] **Dark Mode Toggle** — ✅ Bereits implementiert: Sun/Moon-Toggle im Header, useTheme Hook aktiv.
|
||||||
|
|
||||||
## Niedrige Priorität
|
## Niedrige Priorität
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
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,
|
Search, ChevronLeft, ChevronRight, Save, CheckCircle, XCircle, Pencil, X, Info, Loader2, Download,
|
||||||
} 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 { 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'
|
||||||
|
|
@ -71,6 +72,7 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) {
|
||||||
const [selectedCase, setSelectedCase] = useState<Case | null>(null)
|
const [selectedCase, setSelectedCase] = useState<Case | null>(null)
|
||||||
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)
|
||||||
|
|
||||||
// Debounce search
|
// Debounce search
|
||||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
@ -112,6 +114,34 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) {
|
||||||
setSheetOpen(true)
|
setSheetOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const exportToExcel = async () => {
|
||||||
|
setExporting(true)
|
||||||
|
try {
|
||||||
|
const params: Record<string, string | number> = {}
|
||||||
|
if (jahr !== '__all__') params.jahr = Number(jahr)
|
||||||
|
if (fallgruppe !== '__all__') params.fallgruppe = fallgruppe
|
||||||
|
if (hasIcd !== '__all__') params.has_icd = hasIcd
|
||||||
|
if (debouncedSearch) params.search = debouncedSearch
|
||||||
|
|
||||||
|
const res = await api.get('/cases/export', { params, responseType: 'blob' })
|
||||||
|
const blob = new Blob([res.data], {
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
})
|
||||||
|
const url = window.URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = `Fallliste_${new Date().toISOString().slice(0, 10)}.xlsx`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
} catch {
|
||||||
|
// silently fail — user sees no file download
|
||||||
|
} finally {
|
||||||
|
setExporting(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">
|
||||||
|
|
@ -178,6 +208,14 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) {
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="outline" size="icon" onClick={exportToExcel} disabled={exporting}>
|
||||||
|
{exporting ? <Loader2 className="size-4 animate-spin" /> : <Download className="size-4" />}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Gefilterte Fallliste als Excel exportieren</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue