docs(03-interaction-quality-and-completeness): create phase plan

This commit is contained in:
2026-03-11 22:18:00 +01:00
parent c4ab29cb66
commit 0d40043615
4 changed files with 666 additions and 2 deletions

View File

@@ -0,0 +1,219 @@
---
phase: 03-interaction-quality-and-completeness
plan: 02
type: execute
wave: 1
depends_on: []
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"
---
<objective>
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.
</objective>
<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>
<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
<interfaces>
<!-- CategoriesPage current delete handler (will be replaced) -->
From frontend/src/pages/CategoriesPage.tsx:
```typescript
const handleDelete = async (id: string) => {
await categoriesApi.delete(id)
fetchCategories()
}
```
<!-- Dialog components already imported in CategoriesPage -->
From frontend/src/pages/CategoriesPage.tsx:
```typescript
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
```
<!-- DashboardPage current empty fallback (will be replaced) -->
From frontend/src/pages/DashboardPage.tsx:
```typescript
// 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>
```
<!-- API types -->
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 }
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Create shared EmptyState component and wire into Dashboard and Categories pages</name>
<files>frontend/src/components/EmptyState.tsx, frontend/src/pages/DashboardPage.tsx, frontend/src/pages/CategoriesPage.tsx</files>
<action>
**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.
</action>
<verify>
<automated>cd /home/jean-luc-makiola/Development/projects/SimpleFinanceDash/frontend && bun run build</automated>
</verify>
<done>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.</done>
</task>
<task type="auto">
<name>Task 2: Add delete confirmation dialog with spinner and error handling to CategoriesPage</name>
<files>frontend/src/pages/CategoriesPage.tsx</files>
<action>
Add new state variables:
```typescript
const [pendingDelete, setPendingDelete] = useState<{ id: string; name: string } | null>(null)
const [deleting, setDeleting] = useState(false)
const [deleteError, setDeleteError] = useState<string | null>(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.
</action>
<verify>
<automated>cd /home/jean-luc-makiola/Development/projects/SimpleFinanceDash/frontend && bun run build</automated>
</verify>
<done>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.</done>
</task>
</tasks>
<verification>
- `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
</verification>
<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>
<output>
After completion, create `.planning/phases/03-interaction-quality-and-completeness/03-02-SUMMARY.md`
</output>