docs(03-interaction-quality-and-completeness): create phase plan
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user