223 lines
10 KiB
Markdown
223 lines
10 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<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>
|
|
**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.
|
|
</action>
|
|
<verify>
|
|
<automated>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</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 vitest run src/pages/CategoriesPage.test.tsx && 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>
|