--- phase: 03-interaction-quality-and-completeness plan: 02 type: execute wave: 1 depends_on: ["03-00"] files_modified: - frontend/src/pages/CategoriesPage.tsx - frontend/src/pages/DashboardPage.tsx - frontend/src/components/EmptyState.tsx autonomous: true requirements: [IXTN-05, STATE-01, STATE-02] must_haves: truths: - "Clicking delete on a category opens a confirmation dialog, not an immediate delete" - "Confirming delete executes the API call with spinner; cancelling closes the dialog" - "If delete fails (ON DELETE RESTRICT), error message shows inline in the dialog" - "Dashboard with no budgets shows an empty state with icon, heading, subtext, and Create CTA" - "Categories page with no categories shows an empty state with Add CTA" artifacts: - path: "frontend/src/pages/CategoriesPage.tsx" provides: "Delete confirmation dialog with spinner and error handling" contains: "pendingDelete" - path: "frontend/src/pages/DashboardPage.tsx" provides: "Empty state when list is empty and not loading" contains: "EmptyState" - path: "frontend/src/components/EmptyState.tsx" provides: "Shared empty state component with icon + heading + subtext + CTA" exports: ["EmptyState"] key_links: - from: "frontend/src/pages/CategoriesPage.tsx" to: "categories API delete endpoint" via: "categoriesApi.delete in confirmDelete handler" pattern: "categoriesApi\\.delete" - from: "frontend/src/pages/DashboardPage.tsx" to: "frontend/src/components/EmptyState.tsx" via: "EmptyState import" pattern: "import.*EmptyState" --- Add delete confirmation dialog to CategoriesPage and designed empty states to Dashboard and Categories pages. Purpose: Prevent accidental category deletion with a confirmation step that handles backend constraints gracefully. Replace bare fallback content with designed empty states that guide users toward first actions. Output: CategoriesPage with delete dialog, EmptyState shared component, empty states on Dashboard and Categories pages. @/home/jean-luc-makiola/.claude/get-shit-done/workflows/execute-plan.md @/home/jean-luc-makiola/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/03-interaction-quality-and-completeness/03-CONTEXT.md @.planning/phases/03-interaction-quality-and-completeness/03-RESEARCH.md From frontend/src/pages/CategoriesPage.tsx: ```typescript const handleDelete = async (id: string) => { await categoriesApi.delete(id) fetchCategories() } ``` From frontend/src/pages/CategoriesPage.tsx: ```typescript import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog' ``` From frontend/src/pages/DashboardPage.tsx: ```typescript // Line 93-99: plain Card with text — replace with EmptyState {t('dashboard.noBudgets')} ``` From frontend/src/lib/api.ts: ```typescript export type CategoryType = 'income' | 'bill' | 'variable_expense' | 'debt' | 'saving' | 'investment' export interface Category { id: string; name: string; type: CategoryType; sort_order: number } ``` From frontend/src/components/ui/spinner.tsx: ```typescript export { Spinner } ``` Task 1: Create shared EmptyState component and wire into Dashboard and Categories pages frontend/src/components/EmptyState.tsx, frontend/src/pages/DashboardPage.tsx, frontend/src/pages/CategoriesPage.tsx **Step 0 — Check shadcn registry first (per project skill rules):** Run `bunx --bun shadcn@latest search -q empty` in the frontend directory. If shadcn provides an EmptyState or similar component, use it instead of creating a custom one. If nothing relevant is found (expected), proceed with custom component below. **Create `frontend/src/components/EmptyState.tsx`:** ```typescript interface EmptyStateProps { icon: React.ElementType // lucide-react icon component heading: string subtext: string action?: { label: string; onClick: () => void } } ``` Render: centered flex column with `py-16 text-center`, icon at `size-12 text-muted-foreground`, heading as `font-semibold`, subtext as `text-sm text-muted-foreground`, optional Button with action.label/onClick. **DashboardPage.tsx:** - Import `EmptyState` and `FolderOpen` from lucide-react - Add a new condition: after the loading skeleton block (line 39-47), before the main return, check `list.length === 0 && !loading`. If true, render the budget selector area + an ` setShowCreate(true) }} />` inside the page layout. Keep the existing Create Budget button in the header area as well. - Replace the existing plain Card fallback (the `!current` branch, lines 93-99) with an `` as well — this handles the "budgets exist but none selected" edge case. Use a simpler message: "Select a budget to view your dashboard." **CategoriesPage.tsx:** - Import `EmptyState` and `FolderOpen` from lucide-react - Add `loading` state: `const [loading, setLoading] = useState(true)` — set to `true` initially, set to `false` after `fetchCategories` completes (wrap existing fetch in try/finally with `setLoading(false)`) - After the header div and before the `grouped.map(...)`, add: `{!loading && list.length === 0 && }` - Guard the grouped cards render with `{grouped.length > 0 && grouped.map(...)}` so both empty state and cards don't show simultaneously. cd /home/jean-luc-makiola/Development/projects/SimpleFinanceDash/frontend && bun vitest run src/pages/DashboardPage.test.tsx src/pages/CategoriesPage.test.tsx && bun run build EmptyState component exists and is used in DashboardPage (no-budgets case) and CategoriesPage (no-categories case). CategoriesPage has loading state to prevent empty-state flash. Build passes. Task 2: Add delete confirmation dialog with spinner and error handling to CategoriesPage frontend/src/pages/CategoriesPage.tsx Add new state variables: ```typescript const [pendingDelete, setPendingDelete] = useState<{ id: string; name: string } | null>(null) const [deleting, setDeleting] = useState(false) const [deleteError, setDeleteError] = useState(null) ``` Import `Spinner` from `@/components/ui/spinner` and `DialogDescription` from `@/components/ui/dialog`. Replace `handleDelete`: ```typescript const confirmDelete = async () => { if (!pendingDelete) return setDeleting(true) setDeleteError(null) try { await categoriesApi.delete(pendingDelete.id) setPendingDelete(null) fetchCategories() } catch (err) { setDeleteError(err instanceof Error ? err.message : 'Failed to delete category') } finally { setDeleting(false) } } ``` Change the delete button in each category row from `onClick={() => handleDelete(cat.id)}` to `onClick={() => { setDeleteError(null); setPendingDelete({ id: cat.id, name: cat.name }) }}`. Add a second Dialog (the delete confirmation) after the existing create/edit dialog: ```tsx { if (!open) { setPendingDelete(null); setDeleteError(null) } }}> Delete {pendingDelete?.name}? This cannot be undone. {deleteError &&

{deleteError}

}
``` Remove the old `handleDelete` function entirely. The delete button in the category row now only sets state — no direct API call. **CRITICAL:** The ON DELETE RESTRICT constraint means deleting a category with budget items returns 500. The catch block handles this — the error message displays inline in the dialog. The dialog does NOT auto-close on error, letting the user read the message and dismiss manually.
cd /home/jean-luc-makiola/Development/projects/SimpleFinanceDash/frontend && bun vitest run src/pages/CategoriesPage.test.tsx && bun run build Delete button opens confirmation dialog. Confirm executes delete with spinner. Error from ON DELETE RESTRICT shows inline. Cancel closes dialog. Build passes with zero errors.
- `cd frontend && bun run build` — production build succeeds - `cd frontend && bun vitest run` — full test suite passes - CategoriesPage delete button opens dialog, not immediate delete - DashboardPage shows EmptyState when no budgets exist - CategoriesPage shows EmptyState when no categories exist - Delete confirmation dialog prevents accidental deletion - ON DELETE RESTRICT errors display inline in dialog (not silent failure) - EmptyState component renders icon + heading + subtext + optional CTA - Dashboard empty state shows "Create your first budget" CTA - Categories empty state shows "Add a category" CTA - No empty-state flash on initial page load (loading guard in CategoriesPage) After completion, create `.planning/phases/03-interaction-quality-and-completeness/03-02-SUMMARY.md`