Files
SimpleFinanceDash/.planning/phases/03-interaction-quality-and-completeness/03-02-PLAN.md

10 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
03-interaction-quality-and-completeness 02 execute 1
03-00
frontend/src/pages/CategoriesPage.tsx
frontend/src/pages/DashboardPage.tsx
frontend/src/components/EmptyState.tsx
true
IXTN-05
STATE-01
STATE-02
truths artifacts key_links
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
path provides contains
frontend/src/pages/CategoriesPage.tsx Delete confirmation dialog with spinner and error handling pendingDelete
path provides contains
frontend/src/pages/DashboardPage.tsx Empty state when list is empty and not loading EmptyState
path provides exports
frontend/src/components/EmptyState.tsx Shared empty state component with icon + heading + subtext + CTA
EmptyState
from to via pattern
frontend/src/pages/CategoriesPage.tsx categories API delete endpoint categoriesApi.delete in confirmDelete handler categoriesApi.delete
from to via pattern
frontend/src/pages/DashboardPage.tsx frontend/src/components/EmptyState.tsx EmptyState import 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.

<execution_context> @/home/jean-luc-makiola/.claude/get-shit-done/workflows/execute-plan.md @/home/jean-luc-makiola/.claude/get-shit-done/templates/summary.md </execution_context>

@.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:

import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'

From frontend/src/pages/DashboardPage.tsx:

// Line 93-99: plain Card with text — replace with EmptyState
<Card>
  <CardContent className="py-12 text-center text-muted-foreground">
    {t('dashboard.noBudgets')}
  </CardContent>
</Card>

From frontend/src/lib/api.ts:

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:

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 `<EmptyState icon={FolderOpen} heading="No budgets yet" subtext="Create your first budget to start tracking your finances." action={{ label: "Create your first budget", onClick: () => 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 `<EmptyState>` 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 && <EmptyState icon={FolderOpen} heading="No categories yet" subtext="Add a category to organize your budget." action={{ label: "Add a category", onClick: openCreate }} />}`
- 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
<Dialog open={!!pendingDelete} onOpenChange={(open) => { if (!open) { setPendingDelete(null); setDeleteError(null) } }}>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Delete {pendingDelete?.name}?</DialogTitle>
      <DialogDescription>This cannot be undone.</DialogDescription>
    </DialogHeader>
    {deleteError && <p className="text-sm text-destructive">{deleteError}</p>}
    <DialogFooter>
      <Button variant="outline" onClick={() => setPendingDelete(null)} disabled={deleting}>Cancel</Button>
      <Button variant="destructive" onClick={confirmDelete} disabled={deleting} className="min-w-[80px]">
        {deleting ? <Spinner /> : 'Delete'}
      </Button>
    </DialogFooter>
  </DialogContent>
</Dialog>
```

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

<success_criteria>

  • 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) </success_criteria>
After completion, create `.planning/phases/03-interaction-quality-and-completeness/03-02-SUMMARY.md`