# 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 ( } /> } /> }> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> ) } 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('/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(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]) ``` 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 ( Freigabe-Anfragen {loading ? ( {[1, 2, 3].map((i) => ( ))} ) : requests.length === 0 ? ( Keine offenen Anfragen. ) : ( {requests.map((dr: DisclosureRequest) => ( Fall {dr.fall_id || dr.case_id} {dr.status} Angefragt von:{' '} {dr.requester_username || `User #${dr.requester_id}`} Begründung:{' '} {dr.reason} {new Date(dr.created_at).toLocaleString('de-DE')} handleReview(dr.id, 'approved')} disabled={reviewMutation.isPending} > Genehmigen handleReview(dr.id, 'rejected')} disabled={reviewMutation.isPending} > Ablehnen ))} )} ) } ``` **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 = { 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('/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([]) 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('/admin/users', { params: { skip: 0, limit: 200 } }).then(r => r.data), }) } export function useCreateUser() { const queryClient = useQueryClient() return useMutation({ mutationFn: (data: CreateUserPayload) => api.post('/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(`/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([]) 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('/notifications').then(r => r.data), refetchInterval: 60_000, }) const markAsReadMutation = useMutation({ mutationFn: (id: number) => api.put(`/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('/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([]) 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 = { 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) }, }) } export function usePendingIcdCases(page: number, perPage: number) { 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), }) } 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'] }) }, }) } ``` **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(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(`/cases/${caseData.id}/kvnr`, { kvnr: kvnrVal?.trim() || null }) const res = await kvnrUpdateMutation.mutateAsync({ id: caseData.id, kvnr: kvnrVal?.trim() || null }) // Instead of: api.put(`/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 `` 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
Keine offenen Anfragen.
Angefragt von:{' '} {dr.requester_username || `User #${dr.requester_id}`}
Begründung:{' '} {dr.reason}
{new Date(dr.created_at).toLocaleString('de-DE')}