dak.c2s/docs/plans/2026-02-26-state-management-implementation.md
CCS Admin 62ee46fa3e docs: add state management implementation plan (9 tasks, TanStack Query)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 18:16:20 +00:00

877 lines
26 KiB
Markdown

# State Management Refactoring — Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Introduce TanStack Query for server-state management to eliminate boilerplate and add caching/invalidation across all pages.
**Architecture:** Add `@tanstack/react-query` with a `QueryClientProvider` wrapping the app. Create domain-specific hooks (`useCases`, `useDashboard`, etc.) that encapsulate query keys + API calls. Migrate pages from `useState+useEffect` to `useQuery`/`useMutation`. Existing AuthContext and UI-state patterns remain unchanged.
**Tech Stack:** React 19, TanStack Query v5, TypeScript, Vite, Axios
---
### Task 1: Install TanStack Query and add QueryClientProvider
**Files:**
- Modify: `frontend/package.json`
- Modify: `frontend/src/App.tsx`
**Step 1: Install dependency**
Run: `cd /home/frontend/dak_c2s/frontend && pnpm add @tanstack/react-query`
**Step 2: Add QueryClientProvider to App.tsx**
Replace the contents of `frontend/src/App.tsx` with:
```tsx
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { AuthProvider } from '@/context/AuthContext'
import { ProtectedRoute } from '@/components/layout/ProtectedRoute'
import { AppLayout } from '@/components/layout/AppLayout'
import { LoginPage } from '@/pages/LoginPage'
import { RegisterPage } from '@/pages/RegisterPage'
import { DashboardPage } from '@/pages/DashboardPage'
import { CasesPage } from '@/pages/CasesPage'
import { ImportPage } from '@/pages/ImportPage'
import { IcdPage } from '@/pages/IcdPage'
import { CodingPage } from '@/pages/CodingPage'
import { ReportsPage } from '@/pages/ReportsPage'
import { AdminUsersPage } from '@/pages/AdminUsersPage'
import { AdminInvitationsPage } from '@/pages/AdminInvitationsPage'
import { AdminAuditPage } from '@/pages/AdminAuditPage'
import { DisclosuresPage } from '@/pages/DisclosuresPage'
import { AccountPage } from '@/pages/AccountPage'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
retry: 1,
refetchOnWindowFocus: false,
},
},
})
function App() {
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<AuthProvider>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/" element={<ProtectedRoute><AppLayout /></ProtectedRoute>}>
<Route index element={<Navigate to="/dashboard" replace />} />
<Route path="dashboard" element={<DashboardPage />} />
<Route path="cases" element={<CasesPage />} />
<Route path="import" element={<ProtectedRoute requireAdmin><ImportPage /></ProtectedRoute>} />
<Route path="icd" element={<IcdPage />} />
<Route path="coding" element={<ProtectedRoute requireAdmin><CodingPage /></ProtectedRoute>} />
<Route path="reports" element={<ReportsPage />} />
<Route path="account" element={<AccountPage />} />
<Route path="admin/users" element={<ProtectedRoute requireAdmin><AdminUsersPage /></ProtectedRoute>} />
<Route path="admin/invitations" element={<ProtectedRoute requireAdmin><AdminInvitationsPage /></ProtectedRoute>} />
<Route path="admin/audit" element={<ProtectedRoute requireAdmin><AdminAuditPage /></ProtectedRoute>} />
<Route path="admin/disclosures" element={<ProtectedRoute requireAdmin><DisclosuresPage /></ProtectedRoute>} />
</Route>
</Routes>
</AuthProvider>
</BrowserRouter>
</QueryClientProvider>
)
}
export default App
```
**Step 3: Verify build**
Run: `cd /home/frontend/dak_c2s/frontend && pnpm build`
Expected: Build succeeds without errors.
**Step 4: Commit**
```bash
cd /home/frontend/dak_c2s
git add frontend/package.json frontend/pnpm-lock.yaml frontend/src/App.tsx
git commit -m "feat: add TanStack Query with QueryClientProvider"
```
---
### Task 2: Migrate DashboardPage
**Files:**
- Create: `frontend/src/hooks/useDashboard.ts`
- Modify: `frontend/src/pages/DashboardPage.tsx`
**Step 1: Create useDashboard hook**
Create `frontend/src/hooks/useDashboard.ts`:
```ts
import { useQuery } from '@tanstack/react-query'
import api from '@/services/api'
import type { DashboardResponse } from '@/types'
export function useDashboard(jahr: number) {
return useQuery({
queryKey: ['dashboard', jahr],
queryFn: () => api.get<DashboardResponse>('/reports/dashboard', { params: { jahr } }).then(r => r.data),
})
}
```
**Step 2: Update DashboardPage to use the hook**
In `frontend/src/pages/DashboardPage.tsx`, replace the state + useEffect block (lines 33-43):
Remove:
```tsx
const [data, setData] = useState<DashboardResponse | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
setLoading(true)
api.get<DashboardResponse>('/reports/dashboard', { params: { jahr } })
.then((res) => setData(res.data))
.catch(() => setData(null))
.finally(() => setLoading(false))
}, [jahr])
```
Replace with:
```tsx
const { data, isLoading: loading } = useDashboard(jahr)
```
Update imports: remove `useEffect` from react import, remove `api` import, add:
```tsx
import { useDashboard } from '@/hooks/useDashboard'
```
Keep `useState` for `jahr` (UI state, not server state).
**Step 3: Verify build**
Run: `cd /home/frontend/dak_c2s/frontend && pnpm build`
**Step 4: Commit**
```bash
cd /home/frontend/dak_c2s
git add frontend/src/hooks/useDashboard.ts frontend/src/pages/DashboardPage.tsx
git commit -m "refactor: migrate DashboardPage to TanStack Query"
```
---
### Task 3: Migrate DisclosuresPage
**Files:**
- Create: `frontend/src/hooks/useDisclosures.ts`
- Modify: `frontend/src/pages/DisclosuresPage.tsx`
**Step 1: Create useDisclosures hook**
Create `frontend/src/hooks/useDisclosures.ts`:
```ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { getDisclosureRequests, reviewDisclosure } from '@/services/disclosureService'
export function useDisclosures(status: string = 'pending') {
return useQuery({
queryKey: ['disclosures', status],
queryFn: () => getDisclosureRequests(status),
})
}
export function useReviewDisclosure() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ id, status }: { id: number; status: 'approved' | 'rejected' }) =>
reviewDisclosure(id, status),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['disclosures'] })
queryClient.invalidateQueries({ queryKey: ['notifications'] })
},
})
}
```
**Step 2: Rewrite DisclosuresPage**
Replace the full content of `frontend/src/pages/DisclosuresPage.tsx`:
```tsx
import { Check, X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
import type { DisclosureRequest } from '@/types'
import { useDisclosures, useReviewDisclosure } from '@/hooks/useDisclosures'
export function DisclosuresPage() {
const { data: requests = [], isLoading: loading } = useDisclosures('pending')
const reviewMutation = useReviewDisclosure()
const handleReview = (id: number, status: 'approved' | 'rejected') => {
reviewMutation.mutate({ id, status })
}
return (
<div className="p-6 space-y-4">
<h1 className="text-2xl font-bold">Freigabe-Anfragen</h1>
{loading ? (
<div className="space-y-2">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-24 w-full" />
))}
</div>
) : requests.length === 0 ? (
<p className="text-muted-foreground">Keine offenen Anfragen.</p>
) : (
<div className="space-y-3">
{requests.map((dr: DisclosureRequest) => (
<Card key={dr.id}>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-base">
Fall {dr.fall_id || dr.case_id}
</CardTitle>
<Badge variant="outline">{dr.status}</Badge>
</div>
</CardHeader>
<CardContent className="space-y-2">
<p className="text-sm">
<span className="text-muted-foreground">Angefragt von:</span>{' '}
{dr.requester_username || `User #${dr.requester_id}`}
</p>
<p className="text-sm">
<span className="text-muted-foreground">Begründung:</span>{' '}
{dr.reason}
</p>
<p className="text-xs text-muted-foreground">
{new Date(dr.created_at).toLocaleString('de-DE')}
</p>
<div className="flex gap-2 pt-1">
<Button
size="sm"
onClick={() => handleReview(dr.id, 'approved')}
disabled={reviewMutation.isPending}
>
<Check className="size-4 mr-1" />
Genehmigen
</Button>
<Button
size="sm"
variant="destructive"
onClick={() => handleReview(dr.id, 'rejected')}
disabled={reviewMutation.isPending}
>
<X className="size-4 mr-1" />
Ablehnen
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
)
}
```
**Step 3: Verify build**
Run: `cd /home/frontend/dak_c2s/frontend && pnpm build`
**Step 4: Commit**
```bash
cd /home/frontend/dak_c2s
git add frontend/src/hooks/useDisclosures.ts frontend/src/pages/DisclosuresPage.tsx
git commit -m "refactor: migrate DisclosuresPage to TanStack Query"
```
---
### Task 4: Migrate AdminAuditPage
**Files:**
- Create: `frontend/src/hooks/useAuditLog.ts`
- Modify: `frontend/src/pages/AdminAuditPage.tsx`
**Step 1: Create useAuditLog hook**
Create `frontend/src/hooks/useAuditLog.ts`:
```ts
import { useQuery } from '@tanstack/react-query'
import api from '@/services/api'
import type { AuditLogEntry } from '@/types'
export interface AuditLogFilters {
skip: number
limit: number
user_id?: string
action?: string
date_from?: string
date_to?: string
}
export function useAuditLog(filters: AuditLogFilters) {
const params: Record<string, string | number> = {
skip: filters.skip,
limit: filters.limit,
}
if (filters.user_id) params.user_id = filters.user_id
if (filters.action) params.action = filters.action
if (filters.date_from) params.date_from = filters.date_from
if (filters.date_to) params.date_to = filters.date_to
return useQuery({
queryKey: ['audit-log', params],
queryFn: () => api.get<AuditLogEntry[]>('/admin/audit-log', { params }).then(r => r.data),
})
}
```
**Step 2: Update AdminAuditPage**
In `frontend/src/pages/AdminAuditPage.tsx`:
Remove: `import api from '@/services/api'`
Add: `import { useAuditLog } from '@/hooks/useAuditLog'`
Replace the state + fetch logic (lines 16-18 + 30-46) with:
Remove these lines:
```tsx
const [entries, setEntries] = useState<AuditLogEntry[]>([])
const [loading, setLoading] = useState(true)
```
and the `fetchEntries` function and its `useEffect`.
Replace with:
```tsx
const { data: entries = [], isLoading: loading, refetch } = useAuditLog({
skip, limit, user_id: filterUserId, action: filterAction,
date_from: filterDateFrom, date_to: filterDateTo,
})
```
Update `handleFilter` to call `refetch()` instead of `fetchEntries()`.
Update `resetFilters` to call `refetch()` instead of `setTimeout(fetchEntries, 0)`.
Remove `useEffect` from imports if no longer used. Keep `useState` for filter UI state and `expandedIds`.
**Step 3: Verify build**
Run: `cd /home/frontend/dak_c2s/frontend && pnpm build`
**Step 4: Commit**
```bash
cd /home/frontend/dak_c2s
git add frontend/src/hooks/useAuditLog.ts frontend/src/pages/AdminAuditPage.tsx
git commit -m "refactor: migrate AdminAuditPage to TanStack Query"
```
---
### Task 5: Migrate AdminUsersPage
**Files:**
- Create: `frontend/src/hooks/useUsers.ts`
- Modify: `frontend/src/pages/AdminUsersPage.tsx`
**Step 1: Create useUsers hook**
Create `frontend/src/hooks/useUsers.ts`:
```ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import api from '@/services/api'
import type { UserResponse, CreateUserPayload, UpdateUserPayload } from '@/types'
export function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: () => api.get<UserResponse[]>('/admin/users', { params: { skip: 0, limit: 200 } }).then(r => r.data),
})
}
export function useCreateUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (data: CreateUserPayload) => api.post<UserResponse>('/admin/users', data).then(r => r.data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
})
}
export function useUpdateUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ id, data }: { id: number; data: UpdateUserPayload }) =>
api.put<UserResponse>(`/admin/users/${id}`, data).then(r => r.data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
})
}
```
**Step 2: Update AdminUsersPage**
In `frontend/src/pages/AdminUsersPage.tsx`:
Remove: `import api from '@/services/api'`
Add: `import { useUsers, useCreateUser, useUpdateUser } from '@/hooks/useUsers'`
Replace state + fetch (lines 23-24, 42-52):
```tsx
// Remove:
const [users, setUsers] = useState<UserResponse[]>([])
const [loading, setLoading] = useState(true)
const fetchUsers = () => { ... }
useEffect(() => { fetchUsers() }, [])
// Replace with:
const { data: users = [], isLoading: loading } = useUsers()
const createMutation = useCreateUser()
const updateMutation = useUpdateUser()
```
Update `handleCreate` (lines 55-69):
```tsx
const handleCreate = async () => {
setCreateError('')
try {
await createMutation.mutateAsync(createForm)
setCreateOpen(false)
setCreateForm({ username: '', email: '', password: '', role: 'dak_mitarbeiter' })
} catch (err: unknown) {
const msg = (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail
setCreateError(msg || 'Fehler beim Erstellen des Benutzers.')
}
}
```
Remove `creating` state, use `createMutation.isPending` instead.
Update `handleEdit` (lines 79-94) similarly with `updateMutation.mutateAsync({ id: editUser.id, data: editForm })`.
Remove `editing` state, use `updateMutation.isPending` instead.
**Step 3: Verify build**
Run: `cd /home/frontend/dak_c2s/frontend && pnpm build`
**Step 4: Commit**
```bash
cd /home/frontend/dak_c2s
git add frontend/src/hooks/useUsers.ts frontend/src/pages/AdminUsersPage.tsx
git commit -m "refactor: migrate AdminUsersPage to TanStack Query"
```
---
### Task 6: Migrate useNotifications
**Files:**
- Modify: `frontend/src/hooks/useNotifications.ts`
**Step 1: Rewrite useNotifications with TanStack Query**
Replace the full content of `frontend/src/hooks/useNotifications.ts`:
```ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import api from '@/services/api'
import type { Notification, NotificationList } from '@/types'
export function useNotifications() {
const queryClient = useQueryClient()
const { data, isLoading: loading } = useQuery({
queryKey: ['notifications'],
queryFn: () => api.get<NotificationList>('/notifications').then(r => r.data),
refetchInterval: 60_000,
})
const markAsReadMutation = useMutation({
mutationFn: (id: number) => api.put<Notification>(`/notifications/${id}/read`),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['notifications'] }),
})
const markAllAsReadMutation = useMutation({
mutationFn: () => api.put<{ marked_read: number }>('/notifications/read-all'),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['notifications'] }),
})
return {
notifications: data?.items ?? [],
unreadCount: data?.unread_count ?? 0,
loading,
markAsRead: (id: number) => markAsReadMutation.mutate(id),
markAllAsRead: () => markAllAsReadMutation.mutate(),
refresh: () => queryClient.invalidateQueries({ queryKey: ['notifications'] }),
}
}
```
The public API of this hook is identical — `notifications`, `unreadCount`, `loading`, `markAsRead`, `markAllAsRead`, `refresh` — so no consumers need to change.
**Step 2: Verify build**
Run: `cd /home/frontend/dak_c2s/frontend && pnpm build`
**Step 3: Commit**
```bash
cd /home/frontend/dak_c2s
git add frontend/src/hooks/useNotifications.ts
git commit -m "refactor: migrate useNotifications to TanStack Query"
```
---
### Task 7: Migrate ReportsPage
**Files:**
- Create: `frontend/src/hooks/useReports.ts`
- Modify: `frontend/src/pages/ReportsPage.tsx`
**Step 1: Create useReports hook**
Create `frontend/src/hooks/useReports.ts`:
```ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import api from '@/services/api'
import type { ReportMeta } from '@/types'
export function useReports() {
return useQuery({
queryKey: ['reports'],
queryFn: () => api.get<{ items: ReportMeta[]; total: number }>('/reports/list').then(r => r.data),
})
}
export function useGenerateReport() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ jahr, kw }: { jahr: number; kw: number }) =>
api.post<ReportMeta>('/reports/generate', null, { params: { jahr, kw } }).then(r => r.data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['reports'] }),
})
}
export function useDeleteReports() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (ids: number[]) => api.delete('/reports/delete', { data: ids }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['reports'] }),
})
}
```
**Step 2: Update ReportsPage**
In `frontend/src/pages/ReportsPage.tsx`:
Remove: `import api from '@/services/api'`
Add: `import { useReports, useGenerateReport, useDeleteReports } from '@/hooks/useReports'`
Replace state + fetch logic (lines 22-54):
```tsx
// Remove:
const [reports, setReports] = useState<ReportMeta[]>([])
const [totalReports, setTotalReports] = useState(0)
const [loading, setLoading] = useState(true)
const [generating, setGenerating] = useState(false)
const [deleting, setDeleting] = useState(false)
const fetchReports = () => { ... }
useEffect(() => { fetchReports() }, [])
// Replace with:
const { data: reportData, isLoading: loading } = useReports()
const reports = reportData?.items ?? []
const totalReports = reportData?.total ?? 0
const generateMutation = useGenerateReport()
const deleteMutation = useDeleteReports()
```
Update `generateReport`:
```tsx
const generateReport = async () => {
setGenError('')
setGenSuccess('')
try {
const result = await generateMutation.mutateAsync({ jahr: genJahr, kw: genKw })
setGenSuccess(`Bericht für KW ${result.kw}/${result.jahr} wurde generiert.`)
} catch {
setGenError('Fehler beim Generieren des Berichts.')
}
}
```
Use `generateMutation.isPending` instead of `generating` state.
Update `deleteSelected`:
```tsx
const deleteSelected = async () => {
if (selectedIds.size === 0) return
try {
await deleteMutation.mutateAsync(Array.from(selectedIds))
setSelectedIds(new Set())
} catch {
setGenError('Fehler beim Löschen der Berichte.')
}
}
```
Use `deleteMutation.isPending` instead of `deleting` state.
Remove `useEffect` from imports if no longer used.
**Step 3: Verify build**
Run: `cd /home/frontend/dak_c2s/frontend && pnpm build`
**Step 4: Commit**
```bash
cd /home/frontend/dak_c2s
git add frontend/src/hooks/useReports.ts frontend/src/pages/ReportsPage.tsx
git commit -m "refactor: migrate ReportsPage to TanStack Query"
```
---
### Task 8: Migrate CasesPage (list + mutations)
**Files:**
- Create: `frontend/src/hooks/useCases.ts`
- Modify: `frontend/src/pages/CasesPage.tsx`
- Modify: `frontend/src/pages/cases/useInlineEdit.ts`
**Step 1: Create useCases hook**
Create `frontend/src/hooks/useCases.ts`:
```ts
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) {
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)
},
})
}
export function usePendingIcdCases(page: number, perPage: number) {
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),
})
}
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'] })
},
})
}
```
**Step 2: Update CasesPage**
In `frontend/src/pages/CasesPage.tsx`:
Remove: `import api from '@/services/api'`
Add: `import { useCases, usePendingIcdCases, useIcdUpdate } from '@/hooks/useCases'`
Replace the fetch useEffect (lines 88-110) and state (lines 65-66) with:
```tsx
// Remove:
const [data, setData] = useState<CaseListResponse | null>(null)
const [loading, setLoading] = useState(true)
// Remove the entire useEffect that fetches cases
// Replace with:
const filters: CaseFilters = {
page, per_page: perPage,
...(debouncedSearch ? { search: debouncedSearch } : {}),
...(jahr !== '__all__' ? { jahr: Number(jahr) } : {}),
...(fallgruppe !== '__all__' ? { fallgruppe } : {}),
...(hasIcd !== '__all__' ? { has_icd: hasIcd } : {}),
}
const casesQuery = pendingIcdOnly
? usePendingIcdCases(page, perPage)
: useCases(filters)
const data = casesQuery.data ?? null
const loading = casesQuery.isLoading
```
Note: Import `type { CaseFilters }` from `@/hooks/useCases`.
Remove `handleCaseSaved` function entirely (lines 120-131). TanStack Query cache invalidation handles this automatically via mutations.
Update CaseDetail props: remove `onCaseSaved` prop.
**Step 3: Update useInlineEdit to use mutations**
In `frontend/src/pages/cases/useInlineEdit.ts`:
Remove: `import api from '@/services/api'`
Add: `import { useCaseUpdate, useKvnrUpdate } from '@/hooks/useCases'`
Remove the `onSaved` parameter from the function signature:
```tsx
export function useInlineEdit(caseData: Case): UseInlineEditReturn {
```
Add mutations at the top of the hook:
```tsx
const caseUpdateMutation = useCaseUpdate()
const kvnrUpdateMutation = useKvnrUpdate()
```
In `saveAll`, replace direct API calls:
```tsx
// Instead of: api.put<Case>(`/cases/${caseData.id}/kvnr`, { kvnr: kvnrVal?.trim() || null })
const res = await kvnrUpdateMutation.mutateAsync({ id: caseData.id, kvnr: kvnrVal?.trim() || null })
// Instead of: api.put<Case>(`/cases/${caseData.id}`, payload)
const res = await caseUpdateMutation.mutateAsync({ id: caseData.id, data: payload })
```
Remove the `onSaved(lastResult)` call — cache invalidation from mutations handles list refresh.
**Step 4: Update CaseDetail to remove onCaseSaved**
In CaseDetail component within CasesPage.tsx:
Remove `onCaseSaved` prop and its type. Update ICD save to use `useIcdUpdate()`:
```tsx
const icdMutation = useIcdUpdate()
const saveIcd = async () => {
if (!icdValue.trim()) return
try {
await icdMutation.mutateAsync({ id: caseData.id, icd: icdValue.trim() })
setIcdSuccess(true)
} catch {
setIcdError('Fehler beim Speichern des ICD-Codes.')
}
}
```
Use `icdMutation.isPending` instead of `icdSaving` state. Remove `icdSaving` useState.
Remove `onCaseSaved` from the `<CaseDetail>` JSX call.
**Step 5: Verify build**
Run: `cd /home/frontend/dak_c2s/frontend && pnpm build`
**Step 6: Commit**
```bash
cd /home/frontend/dak_c2s
git add frontend/src/hooks/useCases.ts frontend/src/pages/CasesPage.tsx frontend/src/pages/cases/useInlineEdit.ts
git commit -m "refactor: migrate CasesPage and useInlineEdit to TanStack Query"
```
---
### Task 9: Build, test, deploy
**Step 1: Build frontend**
Run: `cd /home/frontend/dak_c2s/frontend && pnpm build`
Expected: Build succeeds.
**Step 2: Push and merge**
```bash
cd /home/frontend/dak_c2s
git push origin develop
git checkout main && git pull origin main && git merge develop && git push origin main
git checkout develop
```
**Step 3: Deploy to Hetzner 1**
```bash
ssh hetzner1 "cd /opt/dak-portal && git pull origin main"
ssh hetzner1 "cd /opt/dak-portal/frontend && pnpm install && pnpm build && cp -r dist/* /var/www/vhosts/complexcaresolutions.de/dak.complexcaresolutions.de/dist/"
```
**Step 4: Smoke test**
Verify in browser:
1. Dashboard loads with caching (navigate away and back — no reload flash)
2. Cases list loads, detail sheet opens
3. Edit a case field → save → list refreshes automatically
4. ICD save works
5. Notifications badge updates
6. Admin pages (Users, Audit, Disclosures) all work