From 62ee46fa3e05bb7bfb74917b49c0497d98d1289a Mon Sep 17 00:00:00 2001 From: CCS Admin Date: Thu, 26 Feb 2026 18:16:20 +0000 Subject: [PATCH] docs: add state management implementation plan (9 tasks, TanStack Query) Co-Authored-By: Claude Opus 4.6 --- ...6-02-26-state-management-implementation.md | 877 ++++++++++++++++++ 1 file changed, 877 insertions(+) create mode 100644 docs/plans/2026-02-26-state-management-implementation.md diff --git a/docs/plans/2026-02-26-state-management-implementation.md b/docs/plans/2026-02-26-state-management-implementation.md new file mode 100644 index 0000000..c987e7c --- /dev/null +++ b/docs/plans/2026-02-26-state-management-implementation.md @@ -0,0 +1,877 @@ +# 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')} +

+
+ + +
+
+
+ ))} +
+ )} +
+ ) +} +``` + +**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