diff --git a/frontend/src/hooks/useCases.ts b/frontend/src/hooks/useCases.ts new file mode 100644 index 0000000..4ad034d --- /dev/null +++ b/frontend/src/hooks/useCases.ts @@ -0,0 +1,70 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import api from '@/services/api' +import type { Case, CaseListResponse } from '@/types' + +export interface CaseFilters { + page: number + per_page: number + search?: string + jahr?: number + fallgruppe?: string + has_icd?: string +} + +export function useCases(filters: CaseFilters, options?: { enabled?: boolean }) { + return useQuery({ + queryKey: ['cases', filters], + queryFn: () => { + const params: Record = { + page: filters.page, + per_page: filters.per_page, + } + if (filters.search) params.search = filters.search + if (filters.jahr) params.jahr = filters.jahr + if (filters.fallgruppe) params.fallgruppe = filters.fallgruppe + if (filters.has_icd) params.has_icd = filters.has_icd + return api.get('/cases/', { params }).then(r => r.data) + }, + enabled: options?.enabled ?? true, + }) +} + +export function usePendingIcdCases(page: number, perPage: number, options?: { enabled?: boolean }) { + return useQuery({ + queryKey: ['cases', 'pending-icd', { page, per_page: perPage }], + queryFn: () => api.get('/cases/pending-icd', { + params: { page, per_page: perPage }, + }).then(r => r.data), + enabled: options?.enabled ?? true, + }) +} + +export function useCaseUpdate() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: ({ id, data }: { id: number; data: Record }) => + api.put(`/cases/${id}`, data).then(r => r.data), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['cases'] }), + }) +} + +export function useKvnrUpdate() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: ({ id, kvnr }: { id: number; kvnr: string | null }) => + api.put(`/cases/${id}/kvnr`, { kvnr }).then(r => r.data), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['cases'] }), + }) +} + +export function useIcdUpdate() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: ({ id, icd }: { id: number; icd: string }) => + api.put(`/cases/${id}/icd`, { icd }).then(r => r.data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['cases'] }) + queryClient.invalidateQueries({ queryKey: ['dashboard'] }) + }, + }) +} diff --git a/frontend/src/pages/CasesPage.tsx b/frontend/src/pages/CasesPage.tsx index f848161..2f86fe2 100644 --- a/frontend/src/pages/CasesPage.tsx +++ b/frontend/src/pages/CasesPage.tsx @@ -2,9 +2,9 @@ import { useState, useEffect, useCallback, useRef } from 'react' import { Search, ChevronLeft, ChevronRight, Save, CheckCircle, XCircle, Pencil, X, } from 'lucide-react' -import api from '@/services/api' -import type { Case, CaseListResponse } from '@/types' +import type { Case } from '@/types' import { useAuth } from '@/context/AuthContext' +import { useCases, usePendingIcdCases, useIcdUpdate, type CaseFilters } from '@/hooks/useCases' import { Input } from '@/components/ui/input' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' @@ -62,8 +62,6 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) { 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) @@ -84,30 +82,20 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) { } }, []) - // Fetch cases - useEffect(() => { - setLoading(true) + // Fetch cases via TanStack Query + const filters: CaseFilters = { + page, per_page: perPage, + ...(debouncedSearch ? { search: debouncedSearch } : {}), + ...(jahr !== '__all__' ? { jahr: Number(jahr) } : {}), + ...(fallgruppe !== '__all__' ? { fallgruppe } : {}), + ...(hasIcd !== '__all__' ? { has_icd: hasIcd } : {}), + } - 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 casesQuery = useCases(filters, { enabled: !pendingIcdOnly }) + const pendingQuery = usePendingIcdCases(page, perPage, { enabled: pendingIcdOnly }) + const activeQuery = pendingIcdOnly ? pendingQuery : casesQuery + const data = activeQuery.data ?? null + const loading = activeQuery.isLoading const totalPages = data ? Math.ceil(data.total / perPage) : 0 const years = Array.from({ length: 5 }, (_, i) => currentYear - i) @@ -117,19 +105,6 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) { setSheetOpen(true) } - const handleCaseSaved = (updated: Case) => { - setSelectedCase(updated) - // Refresh list - setData((prev) => - prev - ? { - ...prev, - items: prev.items.map((c) => (c.id === updated.id ? updated : c)), - } - : prev, - ) - } - return (

@@ -286,10 +261,7 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) { {selectedCase && ( - + )} @@ -309,22 +281,16 @@ function StatusBadges({ c }: { c: Case }) { ) } -function CaseDetail({ - caseData, - onCaseSaved, -}: { - caseData: Case - onCaseSaved: (updated: Case) => void -}) { +function CaseDetail({ caseData }: { caseData: Case }) { const { isAdmin } = useAuth() const { editing, formValues, saving, error, success, startEditing, cancelEditing, saveAll, setFieldValue, canEditField, isDirty, - } = useInlineEdit(caseData, onCaseSaved) + } = useInlineEdit(caseData) // ICD has its own endpoint and state + const icdMutation = useIcdUpdate() const [icdValue, setIcdValue] = useState(caseData.icd || '') - const [icdSaving, setIcdSaving] = useState(false) const [icdError, setIcdError] = useState('') const [icdSuccess, setIcdSuccess] = useState(false) @@ -336,17 +302,13 @@ function CaseDetail({ const saveIcd = async () => { if (!icdValue.trim()) return - setIcdSaving(true) setIcdError('') setIcdSuccess(false) try { - const res = await api.put(`/cases/${caseData.id}/icd`, { icd: icdValue.trim() }) - onCaseSaved(res.data) + await icdMutation.mutateAsync({ id: caseData.id, icd: icdValue.trim() }) setIcdSuccess(true) } catch { setIcdError('Fehler beim Speichern des ICD-Codes.') - } finally { - setIcdSaving(false) } } @@ -456,10 +418,10 @@ function CaseDetail({

{icdError && ( diff --git a/frontend/src/pages/cases/useInlineEdit.ts b/frontend/src/pages/cases/useInlineEdit.ts index c619184..8c9bd6d 100644 --- a/frontend/src/pages/cases/useInlineEdit.ts +++ b/frontend/src/pages/cases/useInlineEdit.ts @@ -1,6 +1,6 @@ import { useState, useCallback, useEffect } from 'react' -import api from '@/services/api' import type { Case } from '@/types' +import { useCaseUpdate, useKvnrUpdate } from '@/hooks/useCases' import { CASE_SECTIONS, type FieldConfig } from './fieldConfig' type FormValues = Record @@ -43,8 +43,10 @@ function getDirtyFields(original: FormValues, current: FormValues): FormValues { export function useInlineEdit( caseData: Case, - onSaved: (updated: Case) => void, ): UseInlineEditReturn { + const caseUpdateMutation = useCaseUpdate() + const kvnrUpdateMutation = useKvnrUpdate() + const [editing, setEditing] = useState(false) const [formValues, setFormValues] = useState(() => extractFormValues(caseData)) const [originalValues, setOriginalValues] = useState(() => extractFormValues(caseData)) @@ -101,10 +103,11 @@ export function useInlineEdit( // Split: KVNR goes to its own endpoint (accessible by all users) if ('kvnr' in dirty) { const kvnrVal = dirty.kvnr as string | null - const res = await api.put(`/cases/${caseData.id}/kvnr`, { + const kvnrResult = await kvnrUpdateMutation.mutateAsync({ + id: caseData.id, kvnr: kvnrVal?.trim() || null, }) - lastResult = res.data + lastResult = kvnrResult delete dirty.kvnr } @@ -119,12 +122,14 @@ export function useInlineEdit( payload[key] = value } } - const res = await api.put(`/cases/${caseData.id}`, payload) - lastResult = res.data + const updateResult = await caseUpdateMutation.mutateAsync({ + id: caseData.id, + data: payload, + }) + lastResult = updateResult } if (lastResult) { - onSaved(lastResult) setSuccess(true) setEditing(false) } @@ -138,7 +143,7 @@ export function useInlineEdit( } finally { setSaving(false) } - }, [caseData.id, formValues, originalValues, onSaved]) + }, [caseData.id, formValues, originalValues, caseUpdateMutation, kvnrUpdateMutation]) const canEditField = useCallback( (field: FieldConfig, isAdmin: boolean): boolean => {