tr]:last:border-b-0",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
+ return (
+
+ )
+}
+
+function TableHead({ className, ...props }: React.ComponentProps<"th">) {
+ return (
+ [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableCell({ className, ...props }: React.ComponentProps<"td">) {
+ return (
+ | [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableCaption({
+ className,
+ ...props
+}: React.ComponentProps<"caption">) {
+ return (
+
+ )
+}
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+}
diff --git a/frontend/src/components/ui/tabs.tsx b/frontend/src/components/ui/tabs.tsx
new file mode 100644
index 0000000..7bf18aa
--- /dev/null
+++ b/frontend/src/components/ui/tabs.tsx
@@ -0,0 +1,89 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+import { Tabs as TabsPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function Tabs({
+ className,
+ orientation = "horizontal",
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+const tabsListVariants = cva(
+ "rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
+ {
+ variants: {
+ variant: {
+ default: "bg-muted",
+ line: "gap-1 bg-transparent",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+function TabsList({
+ className,
+ variant = "default",
+ ...props
+}: React.ComponentProps &
+ VariantProps) {
+ return (
+
+ )
+}
+
+function TabsTrigger({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function TabsContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
diff --git a/frontend/src/pages/CasesPage.tsx b/frontend/src/pages/CasesPage.tsx
new file mode 100644
index 0000000..f4e6c8f
--- /dev/null
+++ b/frontend/src/pages/CasesPage.tsx
@@ -0,0 +1,461 @@
+import { useState, useEffect, useCallback, useRef } from 'react'
+import {
+ Search, ChevronLeft, ChevronRight, Save, CheckCircle, XCircle,
+} from 'lucide-react'
+import api from '@/services/api'
+import type { Case, CaseListResponse } from '@/types'
+import { Input } from '@/components/ui/input'
+import { Button } from '@/components/ui/button'
+import { Badge } from '@/components/ui/badge'
+import {
+ Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
+} from '@/components/ui/select'
+import {
+ Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
+} from '@/components/ui/table'
+import {
+ Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription,
+} from '@/components/ui/sheet'
+import { Skeleton } from '@/components/ui/skeleton'
+import { Alert, AlertDescription } from '@/components/ui/alert'
+
+const FALLGRUPPEN_LABELS: Record = {
+ onko: 'Onkologie',
+ kardio: 'Kardiologie',
+ intensiv: 'Intensivmedizin',
+ galle: 'Gallenblase',
+ sd: 'Schilddrüse',
+}
+
+const FALLGRUPPEN_OPTIONS = [
+ { value: '__all__', label: 'Alle Fallgruppen' },
+ { value: 'onko', label: 'Onkologie' },
+ { value: 'kardio', label: 'Kardiologie' },
+ { value: 'intensiv', label: 'Intensivmedizin' },
+ { value: 'galle', label: 'Gallenblase' },
+ { value: 'sd', label: 'Schilddrüse' },
+]
+
+const ICD_OPTIONS = [
+ { value: '__all__', label: 'Alle' },
+ { value: 'true', label: 'Mit ICD' },
+ { value: 'false', label: 'Ohne ICD' },
+]
+
+interface CasesPageProps {
+ /** When true, fetches from pending-icd endpoint instead */
+ pendingIcdOnly?: boolean
+}
+
+export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) {
+ const currentYear = new Date().getFullYear()
+ const [search, setSearch] = useState('')
+ const [debouncedSearch, setDebouncedSearch] = useState('')
+ const [jahr, setJahr] = useState('__all__')
+ const [fallgruppe, setFallgruppe] = useState('__all__')
+ const [hasIcd, setHasIcd] = useState('__all__')
+ const [page, setPage] = useState(1)
+ const [perPage] = useState(50)
+ const [data, setData] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [selectedCase, setSelectedCase] = useState(null)
+ const [sheetOpen, setSheetOpen] = useState(false)
+
+ // Debounce search
+ const debounceRef = useRef | null>(null)
+ const handleSearchChange = useCallback((val: string) => {
+ setSearch(val)
+ if (debounceRef.current) clearTimeout(debounceRef.current)
+ debounceRef.current = setTimeout(() => {
+ setDebouncedSearch(val)
+ setPage(1)
+ }, 300)
+ }, [])
+
+ useEffect(() => {
+ return () => {
+ if (debounceRef.current) clearTimeout(debounceRef.current)
+ }
+ }, [])
+
+ // Fetch cases
+ useEffect(() => {
+ setLoading(true)
+
+ if (pendingIcdOnly) {
+ api.get('/cases/pending-icd', {
+ params: { page, per_page: perPage },
+ })
+ .then((res) => setData(res.data))
+ .catch(() => setData(null))
+ .finally(() => setLoading(false))
+ } else {
+ const params: Record = { page, per_page: perPage }
+ if (debouncedSearch) params.search = debouncedSearch
+ if (jahr !== '__all__') params.jahr = Number(jahr)
+ if (fallgruppe !== '__all__') params.fallgruppe = fallgruppe
+ if (hasIcd !== '__all__') params.has_icd = hasIcd
+
+ api.get('/cases/', { params })
+ .then((res) => setData(res.data))
+ .catch(() => setData(null))
+ .finally(() => setLoading(false))
+ }
+ }, [page, perPage, debouncedSearch, jahr, fallgruppe, hasIcd, pendingIcdOnly])
+
+ const totalPages = data ? Math.ceil(data.total / perPage) : 0
+ const years = Array.from({ length: 5 }, (_, i) => currentYear - i)
+
+ const openDetail = (c: Case) => {
+ setSelectedCase(c)
+ setSheetOpen(true)
+ }
+
+ const handleIcdSaved = (updated: Case) => {
+ setSelectedCase(updated)
+ // Refresh list
+ setData((prev) =>
+ prev
+ ? {
+ ...prev,
+ items: prev.items.map((c) => (c.id === updated.id ? updated : c)),
+ }
+ : prev,
+ )
+ }
+
+ return (
+
+
+ {pendingIcdOnly ? 'ICD-Eingabe' : 'Fälle'}
+
+
+ {/* Filter bar */}
+ {!pendingIcdOnly && (
+
+
+
+ handleSearchChange(e.target.value)}
+ className="pl-9"
+ />
+
+
+
+
+
+ )}
+
+ {/* Table */}
+ {loading ? (
+
+ {Array.from({ length: 8 }).map((_, i) => (
+
+ ))}
+
+ ) : data && data.items.length > 0 ? (
+ <>
+
+
+
+ Fall-ID
+ Datum
+ Nachname
+ Vorname
+ Fallgruppe
+ ICD
+ Gutachten
+ Status
+
+
+
+ {data.items.map((c) => (
+ openDetail(c)}
+ >
+
+ {c.fall_id || '-'}
+
+ {formatDate(c.datum)}
+ {c.nachname}
+ {c.vorname || '-'}
+
+
+ {FALLGRUPPEN_LABELS[c.fallgruppe] || c.fallgruppe}
+
+
+
+ {c.icd ? (
+ {c.icd}
+ ) : (
+ -
+ )}
+
+
+ {c.gutachten ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ ))}
+
+
+
+ {/* Pagination */}
+
+
+ {data.total} Fälle insgesamt
+
+
+
+
+ Seite {page} von {totalPages || 1}
+
+
+
+
+ >
+ ) : (
+
+ Keine Fälle gefunden.
+
+ )}
+
+ {/* Detail Sheet */}
+
+
+ {selectedCase && (
+
+ )}
+
+
+
+ )
+}
+
+function StatusBadges({ c }: { c: Case }) {
+ return (
+
+ {c.unterlagen && Unterlagen}
+ {c.gutachten && Gutachten}
+ {c.abgerechnet && Abgerechnet}
+ {c.ablehnung && Abgelehnt}
+ {c.abbruch && Abbruch}
+
+ )
+}
+
+function CaseDetail({
+ caseData,
+ onIcdSaved,
+}: {
+ caseData: Case
+ onIcdSaved: (updated: Case) => void
+}) {
+ const [icdValue, setIcdValue] = useState(caseData.icd || '')
+ const [saving, setSaving] = useState(false)
+ const [error, setError] = useState('')
+ const [success, setSuccess] = useState(false)
+
+ // Reset on case change
+ useEffect(() => {
+ setIcdValue(caseData.icd || '')
+ setError('')
+ setSuccess(false)
+ }, [caseData.id, caseData.icd])
+
+ const saveIcd = async () => {
+ if (!icdValue.trim()) return
+ setSaving(true)
+ setError('')
+ setSuccess(false)
+ try {
+ const res = await api.put(`/cases/${caseData.id}/icd`, { icd: icdValue.trim() })
+ onIcdSaved(res.data)
+ setSuccess(true)
+ } catch {
+ setError('Fehler beim Speichern des ICD-Codes.')
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ return (
+ <>
+
+
+ Fall {caseData.fall_id || `#${caseData.id}`}
+
+
+ {FALLGRUPPEN_LABELS[caseData.fallgruppe] || caseData.fallgruppe} — KW {caseData.kw}/{caseData.jahr}
+
+
+
+
+ {/* Status badges */}
+
+
+
+
+ {/* Patient info */}
+
+
+
+
+
+
+
+
+
+ {/* Case details */}
+
+
+
+
+
+
+
+
+
+ {/* ICD edit */}
+
+
+
+ { setIcdValue(e.target.value); setSuccess(false) }}
+ placeholder="z.B. C50.9"
+ className="flex-1"
+ />
+
+
+ {error && (
+
+ {error}
+
+ )}
+ {success && (
+ ICD-Code gespeichert.
+ )}
+
+
+ {/* Description fields */}
+ {caseData.kurzbeschreibung && (
+
+
+ {caseData.kurzbeschreibung}
+
+ )}
+ {caseData.fragestellung && (
+
+
+ {caseData.fragestellung}
+
+ )}
+ {caseData.kommentar && (
+
+
+ {caseData.kommentar}
+
+ )}
+
+
+ Importiert: {formatDateTime(caseData.imported_at)}
+ Aktualisiert: {formatDateTime(caseData.updated_at)}
+ {caseData.import_source && Quelle: {caseData.import_source} }
+
+
+ >
+ )
+}
+
+function DetailField({ label, value }: { label: string; value: string | null | undefined }) {
+ return (
+
+ {label}
+ {value || '-'}
+
+ )
+}
+
+function formatDate(dateStr: string): string {
+ try {
+ return new Date(dateStr).toLocaleDateString('de-DE', {
+ day: '2-digit', month: '2-digit', year: 'numeric',
+ })
+ } catch {
+ return dateStr
+ }
+}
+
+function formatDateTime(dateStr: string): string {
+ try {
+ return new Date(dateStr).toLocaleString('de-DE', {
+ day: '2-digit', month: '2-digit', year: 'numeric',
+ hour: '2-digit', minute: '2-digit',
+ })
+ } catch {
+ return dateStr
+ }
+}
diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx
new file mode 100644
index 0000000..458a153
--- /dev/null
+++ b/frontend/src/pages/DashboardPage.tsx
@@ -0,0 +1,220 @@
+import { useState, useEffect } from 'react'
+import {
+ BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend,
+ PieChart, Pie, Cell, ResponsiveContainer,
+} from 'recharts'
+import { FileText, Clock, Code, Stethoscope } from 'lucide-react'
+import api from '@/services/api'
+import type { DashboardResponse } from '@/types'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import {
+ Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
+} from '@/components/ui/select'
+import { Skeleton } from '@/components/ui/skeleton'
+
+const FALLGRUPPEN_LABELS: Record = {
+ onko: 'Onkologie',
+ kardio: 'Kardiologie',
+ intensiv: 'Intensivmedizin',
+ galle: 'Gallenblase',
+ sd: 'Schilddrüse',
+}
+
+const CHART_COLORS = [
+ 'var(--chart-1)',
+ 'var(--chart-2)',
+ 'var(--chart-3)',
+ 'var(--chart-4)',
+ 'var(--chart-5)',
+]
+
+export function DashboardPage() {
+ const currentYear = new Date().getFullYear()
+ const [jahr, setJahr] = useState(currentYear)
+ const [data, setData] = useState(null)
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ setLoading(true)
+ api.get('/reports/dashboard', { params: { jahr } })
+ .then((res) => setData(res.data))
+ .catch(() => setData(null))
+ .finally(() => setLoading(false))
+ }, [jahr])
+
+ const fallgruppenData = data
+ ? Object.entries(data.kpis.fallgruppen).map(([key, value]) => ({
+ name: FALLGRUPPEN_LABELS[key] || key,
+ value,
+ }))
+ : []
+
+ const years = Array.from({ length: 5 }, (_, i) => currentYear - i)
+
+ return (
+
+ {/* Header with year selector */}
+
+ Dashboard
+
+
+
+ {/* KPI Cards */}
+ {loading ? (
+
+ {[1, 2, 3, 4].map((i) => (
+
+
+
+
+
+
+
+
+ ))}
+
+ ) : data ? (
+
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+
+ ) : (
+ Keine Daten verfügbar.
+ )}
+
+ {/* Charts row */}
+ {data && (
+
+ {/* Weekly bar chart - takes 2 cols */}
+
+
+ Wöchentliche Übersicht
+
+
+
+
+
+ `KW ${v}`}
+ className="text-xs"
+ />
+
+ `KW ${v}`}
+ contentStyle={{
+ backgroundColor: 'var(--popover)',
+ border: '1px solid var(--border)',
+ borderRadius: '8px',
+ color: 'var(--popover-foreground)',
+ }}
+ />
+
+
+
+
+
+
+
+
+
+ {/* Fallgruppen pie chart */}
+
+
+ Fallgruppen
+
+
+
+
+ `${name}: ${value}`}
+ >
+ {fallgruppenData.map((_, idx) => (
+ |
+ ))}
+
+
+
+
+
+
+
+ )}
+
+ )
+}
+
+function KpiCard({ title, value, icon }: { title: string; value: number; icon: React.ReactNode }) {
+ return (
+
+
+ {title}
+ {icon}
+
+
+ {value.toLocaleString('de-DE')}
+
+
+ )
+}
diff --git a/frontend/src/pages/IcdPage.tsx b/frontend/src/pages/IcdPage.tsx
new file mode 100644
index 0000000..8bb5950
--- /dev/null
+++ b/frontend/src/pages/IcdPage.tsx
@@ -0,0 +1,5 @@
+import { CasesPage } from './CasesPage'
+
+export function IcdPage() {
+ return
+}
diff --git a/frontend/src/pages/ImportPage.tsx b/frontend/src/pages/ImportPage.tsx
new file mode 100644
index 0000000..3c66876
--- /dev/null
+++ b/frontend/src/pages/ImportPage.tsx
@@ -0,0 +1,550 @@
+import { useState, useEffect, useCallback, useRef } from 'react'
+import {
+ Upload, FileUp, Check, AlertTriangle, ChevronLeft, ChevronRight, History,
+} from 'lucide-react'
+import api from '@/services/api'
+import type {
+ ImportPreview, ImportResult, ICDImportResponse,
+ ImportLogEntry, ImportLogListResponse,
+} from '@/types'
+import { Button } from '@/components/ui/button'
+import { Badge } from '@/components/ui/badge'
+import { Alert, AlertDescription } from '@/components/ui/alert'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import {
+ Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
+} from '@/components/ui/table'
+import {
+ Tabs, TabsContent, TabsList, TabsTrigger,
+} from '@/components/ui/tabs'
+import { Skeleton } from '@/components/ui/skeleton'
+
+export function ImportPage() {
+ return (
+
+ Import
+
+
+
+
+ CSV-Import
+
+
+
+ ICD-Import
+
+
+
+ Import-Verlauf
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+// ---------------------------------------------------------------------------
+// CSV Import Tab
+// ---------------------------------------------------------------------------
+
+function CsvImportTab() {
+ const [file, setFile] = useState(null)
+ const [uploading, setUploading] = useState(false)
+ const [confirming, setConfirming] = useState(false)
+ const [preview, setPreview] = useState(null)
+ const [result, setResult] = useState(null)
+ const [error, setError] = useState('')
+ const fileInputRef = useRef(null)
+
+ const handleDrop = useCallback((e: React.DragEvent) => {
+ e.preventDefault()
+ const f = e.dataTransfer.files[0]
+ if (f && f.name.toLowerCase().endsWith('.csv')) {
+ setFile(f)
+ setPreview(null)
+ setResult(null)
+ setError('')
+ }
+ }, [])
+
+ const handleFileSelect = (e: React.ChangeEvent) => {
+ const f = e.target.files?.[0]
+ if (f) {
+ setFile(f)
+ setPreview(null)
+ setResult(null)
+ setError('')
+ }
+ }
+
+ const uploadPreview = async () => {
+ if (!file) return
+ setUploading(true)
+ setError('')
+ try {
+ const formData = new FormData()
+ formData.append('file', file)
+ const res = await api.post('/import/csv', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ })
+ setPreview(res.data)
+ } catch (err: unknown) {
+ const msg = err instanceof Error ? err.message : 'Upload fehlgeschlagen'
+ setError(msg)
+ } finally {
+ setUploading(false)
+ }
+ }
+
+ const confirmImport = async () => {
+ if (!file) return
+ setConfirming(true)
+ setError('')
+ try {
+ const formData = new FormData()
+ formData.append('file', file)
+ const res = await api.post('/import/csv/confirm', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ })
+ setResult(res.data)
+ setPreview(null)
+ } catch (err: unknown) {
+ const msg = err instanceof Error ? err.message : 'Import fehlgeschlagen'
+ setError(msg)
+ } finally {
+ setConfirming(false)
+ }
+ }
+
+ const reset = () => {
+ setFile(null)
+ setPreview(null)
+ setResult(null)
+ setError('')
+ if (fileInputRef.current) fileInputRef.current.value = ''
+ }
+
+ return (
+
+ {/* Drop zone */}
+ {!preview && !result && (
+ e.preventDefault()}
+ onDrop={handleDrop}
+ className="border-2 border-dashed rounded-lg p-10 text-center cursor-pointer hover:border-primary/50 transition-colors"
+ onClick={() => fileInputRef.current?.click()}
+ >
+
+
+ {file ? (
+
+ {file.name}
+
+ ({(file.size / 1024).toFixed(1)} KB)
+
+
+ ) : (
+
+ CSV-Datei hierhin ziehen
+
+ oder klicken zum Auswählen
+
+
+ )}
+
+ )}
+
+ {file && !preview && !result && (
+
+
+
+
+ )}
+
+ {error && (
+
+
+ {error}
+
+ )}
+
+ {/* Preview */}
+ {preview && (
+
+
+
+
+ Vorschau: {preview.filename}
+
+
+
+
+
+ Zeilen gesamt:{' '}
+ {preview.total_rows}
+
+
+ Neue Fälle:{' '}
+ {preview.new_cases}
+
+
+ Duplikate:{' '}
+ {preview.duplicates}
+
+ {preview.errors.length > 0 && (
+
+ Fehler:{' '}
+ {preview.errors.length}
+
+ )}
+
+
+
+
+ {preview.errors.length > 0 && (
+
+
+
+
+ {preview.errors.map((e, i) => - {e}
)}
+
+
+
+ )}
+
+ {/* Preview table */}
+
+
+
+ #
+ Nachname
+ Vorname
+ Fallgruppe
+ Datum
+ Status
+
+
+
+ {preview.rows.map((row) => (
+
+ {row.row_number}
+ {row.nachname}
+ {row.vorname || '-'}
+ {row.fallgruppe}
+ {row.datum}
+
+ {row.is_duplicate ? (
+
+ Duplikat
+
+ ) : (
+
+ Neu
+
+ )}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Result */}
+ {result && (
+
+
+
+
+ Import abgeschlossen: {result.imported} importiert, {result.skipped} übersprungen, {result.updated} aktualisiert.
+
+
+ {result.errors.length > 0 && (
+
+
+
+
+ {result.errors.map((e, i) => - {e}
)}
+
+
+
+ )}
+
+
+ )}
+
+ )
+}
+
+// ---------------------------------------------------------------------------
+// ICD Import Tab
+// ---------------------------------------------------------------------------
+
+function IcdImportTab() {
+ const [file, setFile] = useState(null)
+ const [uploading, setUploading] = useState(false)
+ const [result, setResult] = useState(null)
+ const [error, setError] = useState('')
+ const fileInputRef = useRef(null)
+
+ const handleDrop = useCallback((e: React.DragEvent) => {
+ e.preventDefault()
+ const f = e.dataTransfer.files[0]
+ if (f && (f.name.toLowerCase().endsWith('.xlsx') || f.name.toLowerCase().endsWith('.xls'))) {
+ setFile(f)
+ setResult(null)
+ setError('')
+ }
+ }, [])
+
+ const handleFileSelect = (e: React.ChangeEvent) => {
+ const f = e.target.files?.[0]
+ if (f) {
+ setFile(f)
+ setResult(null)
+ setError('')
+ }
+ }
+
+ const upload = async () => {
+ if (!file) return
+ setUploading(true)
+ setError('')
+ try {
+ const formData = new FormData()
+ formData.append('file', file)
+ const res = await api.post('/import/icd-xlsx', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ })
+ setResult(res.data)
+ } catch (err: unknown) {
+ const msg = err instanceof Error ? err.message : 'Upload fehlgeschlagen'
+ setError(msg)
+ } finally {
+ setUploading(false)
+ }
+ }
+
+ const reset = () => {
+ setFile(null)
+ setResult(null)
+ setError('')
+ if (fileInputRef.current) fileInputRef.current.value = ''
+ }
+
+ return (
+
+ {!result && (
+ e.preventDefault()}
+ onDrop={handleDrop}
+ className="border-2 border-dashed rounded-lg p-10 text-center cursor-pointer hover:border-primary/50 transition-colors"
+ onClick={() => fileInputRef.current?.click()}
+ >
+
+
+ {file ? (
+
+ {file.name}
+
+ ({(file.size / 1024).toFixed(1)} KB)
+
+
+ ) : (
+
+ Excel-Datei (.xlsx) hierhin ziehen
+
+ oder klicken zum Auswählen
+
+
+ )}
+
+ )}
+
+ {file && !result && (
+
+
+
+
+ )}
+
+ {error && (
+
+
+ {error}
+
+ )}
+
+ {result && (
+
+
+
+
+ ICD-Import abgeschlossen: {result.updated} Fälle aktualisiert.
+
+
+ {result.errors.length > 0 && (
+
+
+
+
+ {result.errors.map((e, i) => - {e}
)}
+
+
+
+ )}
+
+
+ )}
+
+ )
+}
+
+// ---------------------------------------------------------------------------
+// Import Log Tab
+// ---------------------------------------------------------------------------
+
+function ImportLogTab() {
+ const [logs, setLogs] = useState([])
+ const [total, setTotal] = useState(0)
+ const [page, setPage] = useState(1)
+ const [loading, setLoading] = useState(true)
+ const perPage = 20
+
+ useEffect(() => {
+ setLoading(true)
+ api.get('/import/log', {
+ params: { page, per_page: perPage },
+ })
+ .then((res) => {
+ setLogs(res.data.items)
+ setTotal(res.data.total)
+ })
+ .catch(() => {
+ setLogs([])
+ setTotal(0)
+ })
+ .finally(() => setLoading(false))
+ }, [page])
+
+ const totalPages = Math.ceil(total / perPage)
+
+ if (loading) {
+ return (
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ))}
+
+ )
+ }
+
+ if (logs.length === 0) {
+ return Keine Import-Einträge vorhanden.
+ }
+
+ return (
+
+
+
+
+ Datum
+ Dateiname
+ Typ
+ Importiert
+ Übersprungen
+ Fehler
+
+
+
+ {logs.map((log) => (
+
+ {formatDateTime(log.imported_at)}
+ {log.filename}
+
+ {log.import_type}
+
+ {log.imported_rows}
+ {log.skipped_rows}
+
+ {log.error_count > 0 ? (
+ {log.error_count}
+ ) : (
+ '0'
+ )}
+
+
+ ))}
+
+
+
+ {totalPages > 1 && (
+
+
+
+ Seite {page} von {totalPages}
+
+
+
+ )}
+
+ )
+}
+
+function formatDateTime(dateStr: string): string {
+ try {
+ return new Date(dateStr).toLocaleString('de-DE', {
+ day: '2-digit', month: '2-digit', year: 'numeric',
+ hour: '2-digit', minute: '2-digit',
+ })
+ } catch {
+ return dateStr
+ }
+}
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index 52c53eb..e83daea 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -131,3 +131,27 @@ export interface ReportMeta {
report_date: string
generated_at: string
}
+
+export interface ICDImportResponse {
+ updated: number
+ errors: string[]
+}
+
+export interface ImportLogEntry {
+ id: number
+ filename: string
+ import_type: string
+ total_rows: number
+ imported_rows: number
+ skipped_rows: number
+ error_count: number
+ imported_by: number
+ imported_at: string
+}
+
+export interface ImportLogListResponse {
+ items: ImportLogEntry[]
+ total: number
+ page: number
+ per_page: number
+}
|