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

26 KiB

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:

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

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:

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:

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:

const { data, isLoading: loading } = useDashboard(jahr)

Update imports: remove useEffect from react import, remove api import, add:

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

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:

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:

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

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:

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:

const [entries, setEntries] = useState<AuditLogEntry[]>([])
const [loading, setLoading] = useState(true)

and the fetchEntries function and its useEffect.

Replace with:

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

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:

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):

// 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):

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

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:

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

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:

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):

// 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:

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:

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

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:

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:

// 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:

export function useInlineEdit(caseData: Case): UseInlineEditReturn {

Add mutations at the top of the hook:

const caseUpdateMutation = useCaseUpdate()
const kvnrUpdateMutation = useKvnrUpdate()

In saveAll, replace direct API calls:

// 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():

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

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

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

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