mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-17 21:53:41 +00:00
refactor: migrate CasesPage and useInlineEdit to TanStack Query
Replace manual useEffect data fetching with useCases/usePendingIcdCases query hooks. Replace direct API calls in useInlineEdit with useCaseUpdate and useKvnrUpdate mutations. Use useIcdUpdate for ICD saving. Remove onCaseSaved callback prop drilling — mutations auto-invalidate the cache. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
150be9183c
commit
657a1abcaf
3 changed files with 105 additions and 68 deletions
70
frontend/src/hooks/useCases.ts
Normal file
70
frontend/src/hooks/useCases.ts
Normal file
|
|
@ -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<string, string | number> = {
|
||||||
|
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<CaseListResponse>('/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<CaseListResponse>('/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<string, unknown> }) =>
|
||||||
|
api.put<Case>(`/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<Case>(`/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<Case>(`/cases/${id}/icd`, { icd }).then(r => r.data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['cases'] })
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -2,9 +2,9 @@ import { useState, useEffect, useCallback, useRef } from 'react'
|
||||||
import {
|
import {
|
||||||
Search, ChevronLeft, ChevronRight, Save, CheckCircle, XCircle, Pencil, X,
|
Search, ChevronLeft, ChevronRight, Save, CheckCircle, XCircle, Pencil, X,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import api from '@/services/api'
|
import type { Case } from '@/types'
|
||||||
import type { Case, CaseListResponse } from '@/types'
|
|
||||||
import { useAuth } from '@/context/AuthContext'
|
import { useAuth } from '@/context/AuthContext'
|
||||||
|
import { useCases, usePendingIcdCases, useIcdUpdate, type CaseFilters } from '@/hooks/useCases'
|
||||||
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'
|
||||||
|
|
@ -62,8 +62,6 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) {
|
||||||
const [hasIcd, setHasIcd] = useState<string>('__all__')
|
const [hasIcd, setHasIcd] = useState<string>('__all__')
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [perPage] = useState(50)
|
const [perPage] = useState(50)
|
||||||
const [data, setData] = useState<CaseListResponse | null>(null)
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [selectedCase, setSelectedCase] = useState<Case | null>(null)
|
const [selectedCase, setSelectedCase] = useState<Case | null>(null)
|
||||||
const [sheetOpen, setSheetOpen] = useState(false)
|
const [sheetOpen, setSheetOpen] = useState(false)
|
||||||
|
|
||||||
|
|
@ -84,30 +82,20 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) {
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Fetch cases
|
// Fetch cases via TanStack Query
|
||||||
useEffect(() => {
|
const filters: CaseFilters = {
|
||||||
setLoading(true)
|
page, per_page: perPage,
|
||||||
|
...(debouncedSearch ? { search: debouncedSearch } : {}),
|
||||||
|
...(jahr !== '__all__' ? { jahr: Number(jahr) } : {}),
|
||||||
|
...(fallgruppe !== '__all__' ? { fallgruppe } : {}),
|
||||||
|
...(hasIcd !== '__all__' ? { has_icd: hasIcd } : {}),
|
||||||
|
}
|
||||||
|
|
||||||
if (pendingIcdOnly) {
|
const casesQuery = useCases(filters, { enabled: !pendingIcdOnly })
|
||||||
api.get<CaseListResponse>('/cases/pending-icd', {
|
const pendingQuery = usePendingIcdCases(page, perPage, { enabled: pendingIcdOnly })
|
||||||
params: { page, per_page: perPage },
|
const activeQuery = pendingIcdOnly ? pendingQuery : casesQuery
|
||||||
})
|
const data = activeQuery.data ?? null
|
||||||
.then((res) => setData(res.data))
|
const loading = activeQuery.isLoading
|
||||||
.catch(() => setData(null))
|
|
||||||
.finally(() => setLoading(false))
|
|
||||||
} else {
|
|
||||||
const params: Record<string, string | number> = { 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<CaseListResponse>('/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 totalPages = data ? Math.ceil(data.total / perPage) : 0
|
||||||
const years = Array.from({ length: 5 }, (_, i) => currentYear - i)
|
const years = Array.from({ length: 5 }, (_, i) => currentYear - i)
|
||||||
|
|
@ -117,19 +105,6 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) {
|
||||||
setSheetOpen(true)
|
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 (
|
return (
|
||||||
<div className="p-6 space-y-4">
|
<div className="p-6 space-y-4">
|
||||||
<h1 className="text-2xl font-bold">
|
<h1 className="text-2xl font-bold">
|
||||||
|
|
@ -286,10 +261,7 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) {
|
||||||
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
||||||
<SheetContent className="w-full sm:max-w-lg overflow-y-auto">
|
<SheetContent className="w-full sm:max-w-lg overflow-y-auto">
|
||||||
{selectedCase && (
|
{selectedCase && (
|
||||||
<CaseDetail
|
<CaseDetail caseData={selectedCase} />
|
||||||
caseData={selectedCase}
|
|
||||||
onCaseSaved={handleCaseSaved}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
|
|
@ -309,22 +281,16 @@ function StatusBadges({ c }: { c: Case }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CaseDetail({
|
function CaseDetail({ caseData }: { caseData: Case }) {
|
||||||
caseData,
|
|
||||||
onCaseSaved,
|
|
||||||
}: {
|
|
||||||
caseData: Case
|
|
||||||
onCaseSaved: (updated: Case) => void
|
|
||||||
}) {
|
|
||||||
const { isAdmin } = useAuth()
|
const { isAdmin } = useAuth()
|
||||||
const {
|
const {
|
||||||
editing, formValues, saving, error, success,
|
editing, formValues, saving, error, success,
|
||||||
startEditing, cancelEditing, saveAll, setFieldValue, canEditField, isDirty,
|
startEditing, cancelEditing, saveAll, setFieldValue, canEditField, isDirty,
|
||||||
} = useInlineEdit(caseData, onCaseSaved)
|
} = useInlineEdit(caseData)
|
||||||
|
|
||||||
// ICD has its own endpoint and state
|
// ICD has its own endpoint and state
|
||||||
|
const icdMutation = useIcdUpdate()
|
||||||
const [icdValue, setIcdValue] = useState(caseData.icd || '')
|
const [icdValue, setIcdValue] = useState(caseData.icd || '')
|
||||||
const [icdSaving, setIcdSaving] = useState(false)
|
|
||||||
const [icdError, setIcdError] = useState('')
|
const [icdError, setIcdError] = useState('')
|
||||||
const [icdSuccess, setIcdSuccess] = useState(false)
|
const [icdSuccess, setIcdSuccess] = useState(false)
|
||||||
|
|
||||||
|
|
@ -336,17 +302,13 @@ function CaseDetail({
|
||||||
|
|
||||||
const saveIcd = async () => {
|
const saveIcd = async () => {
|
||||||
if (!icdValue.trim()) return
|
if (!icdValue.trim()) return
|
||||||
setIcdSaving(true)
|
|
||||||
setIcdError('')
|
setIcdError('')
|
||||||
setIcdSuccess(false)
|
setIcdSuccess(false)
|
||||||
try {
|
try {
|
||||||
const res = await api.put<Case>(`/cases/${caseData.id}/icd`, { icd: icdValue.trim() })
|
await icdMutation.mutateAsync({ id: caseData.id, icd: icdValue.trim() })
|
||||||
onCaseSaved(res.data)
|
|
||||||
setIcdSuccess(true)
|
setIcdSuccess(true)
|
||||||
} catch {
|
} catch {
|
||||||
setIcdError('Fehler beim Speichern des ICD-Codes.')
|
setIcdError('Fehler beim Speichern des ICD-Codes.')
|
||||||
} finally {
|
|
||||||
setIcdSaving(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -456,10 +418,10 @@ function CaseDetail({
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={saveIcd}
|
onClick={saveIcd}
|
||||||
disabled={icdSaving || !icdValue.trim()}
|
disabled={icdMutation.isPending || !icdValue.trim()}
|
||||||
>
|
>
|
||||||
<Save className="size-4 mr-1" />
|
<Save className="size-4 mr-1" />
|
||||||
{icdSaving ? 'Speichern...' : 'Speichern'}
|
{icdMutation.isPending ? 'Speichern...' : 'Speichern'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{icdError && (
|
{icdError && (
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState, useCallback, useEffect } from 'react'
|
import { useState, useCallback, useEffect } from 'react'
|
||||||
import api from '@/services/api'
|
|
||||||
import type { Case } from '@/types'
|
import type { Case } from '@/types'
|
||||||
|
import { useCaseUpdate, useKvnrUpdate } from '@/hooks/useCases'
|
||||||
import { CASE_SECTIONS, type FieldConfig } from './fieldConfig'
|
import { CASE_SECTIONS, type FieldConfig } from './fieldConfig'
|
||||||
|
|
||||||
type FormValues = Record<string, unknown>
|
type FormValues = Record<string, unknown>
|
||||||
|
|
@ -43,8 +43,10 @@ function getDirtyFields(original: FormValues, current: FormValues): FormValues {
|
||||||
|
|
||||||
export function useInlineEdit(
|
export function useInlineEdit(
|
||||||
caseData: Case,
|
caseData: Case,
|
||||||
onSaved: (updated: Case) => void,
|
|
||||||
): UseInlineEditReturn {
|
): UseInlineEditReturn {
|
||||||
|
const caseUpdateMutation = useCaseUpdate()
|
||||||
|
const kvnrUpdateMutation = useKvnrUpdate()
|
||||||
|
|
||||||
const [editing, setEditing] = useState(false)
|
const [editing, setEditing] = useState(false)
|
||||||
const [formValues, setFormValues] = useState<FormValues>(() => extractFormValues(caseData))
|
const [formValues, setFormValues] = useState<FormValues>(() => extractFormValues(caseData))
|
||||||
const [originalValues, setOriginalValues] = useState<FormValues>(() => extractFormValues(caseData))
|
const [originalValues, setOriginalValues] = useState<FormValues>(() => extractFormValues(caseData))
|
||||||
|
|
@ -101,10 +103,11 @@ export function useInlineEdit(
|
||||||
// Split: KVNR goes to its own endpoint (accessible by all users)
|
// Split: KVNR goes to its own endpoint (accessible by all users)
|
||||||
if ('kvnr' in dirty) {
|
if ('kvnr' in dirty) {
|
||||||
const kvnrVal = dirty.kvnr as string | null
|
const kvnrVal = dirty.kvnr as string | null
|
||||||
const res = await api.put<Case>(`/cases/${caseData.id}/kvnr`, {
|
const kvnrResult = await kvnrUpdateMutation.mutateAsync({
|
||||||
|
id: caseData.id,
|
||||||
kvnr: kvnrVal?.trim() || null,
|
kvnr: kvnrVal?.trim() || null,
|
||||||
})
|
})
|
||||||
lastResult = res.data
|
lastResult = kvnrResult
|
||||||
delete dirty.kvnr
|
delete dirty.kvnr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,12 +122,14 @@ export function useInlineEdit(
|
||||||
payload[key] = value
|
payload[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const res = await api.put<Case>(`/cases/${caseData.id}`, payload)
|
const updateResult = await caseUpdateMutation.mutateAsync({
|
||||||
lastResult = res.data
|
id: caseData.id,
|
||||||
|
data: payload,
|
||||||
|
})
|
||||||
|
lastResult = updateResult
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastResult) {
|
if (lastResult) {
|
||||||
onSaved(lastResult)
|
|
||||||
setSuccess(true)
|
setSuccess(true)
|
||||||
setEditing(false)
|
setEditing(false)
|
||||||
}
|
}
|
||||||
|
|
@ -138,7 +143,7 @@ export function useInlineEdit(
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false)
|
setSaving(false)
|
||||||
}
|
}
|
||||||
}, [caseData.id, formValues, originalValues, onSaved])
|
}, [caseData.id, formValues, originalValues, caseUpdateMutation, kvnrUpdateMutation])
|
||||||
|
|
||||||
const canEditField = useCallback(
|
const canEditField = useCallback(
|
||||||
(field: FieldConfig, isAdmin: boolean): boolean => {
|
(field: FieldConfig, isAdmin: boolean): boolean => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue