diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md new file mode 100644 index 0000000..c64cb90 --- /dev/null +++ b/.planning/codebase/ARCHITECTURE.md @@ -0,0 +1,240 @@ +# Architecture + +**Analysis Date:** 2026-03-16 + +## Pattern Overview + +**Overall:** Layered client-side SPA with React, using hooks for data access and state management through TanStack Query, centralized authentication with Supabase, and component-based UI rendering. + +**Key Characteristics:** +- Three-tier vertical slice: Pages → Hooks → Library (Supabase + utilities) +- TanStack Query for caching and synchronization of remote state +- Route-based code organization with role-based access control (protected/public routes) +- Supabase PostgreSQL backend with real-time subscriptions capability +- Internationalization (i18n) at the root level with JSON resource files + +## Layers + +**Presentation Layer (Pages & Components):** +- Purpose: Render UI, handle user interactions, coordinate component composition +- Location: `src/pages/` and `src/components/` +- Contains: Page components (DashboardPage, TemplatePage, BudgetListPage, etc.), UI primitives from `src/components/ui/`, and custom components (AppLayout, QuickAddPicker) +- Depends on: Hooks, UI utilities, i18n, icons (lucide-react), formatting utilities +- Used by: React Router for page routing + +**Hooks Layer (Data Access & State):** +- Purpose: Encapsulate all server communication and query/mutation logic, manage request/response caching via TanStack Query +- Location: `src/hooks/` +- Contains: Custom hooks for each domain (useAuth, useCategories, useBudgets, useTemplate, useQuickAdd) +- Depends on: Supabase client, TanStack Query, type definitions +- Used by: Page and component layers for CRUD operations and state retrieval + +**Library Layer (Core Services & Utilities):** +- Purpose: Provide primitive implementations, type definitions, and configuration +- Location: `src/lib/` +- Contains: + - `supabase.ts` - Supabase client initialization and environment validation + - `types.ts` - TypeScript interfaces for all domain entities (Profile, Category, Budget, BudgetItem, Template, TemplateItem, QuickAddItem) + - `format.ts` - Currency formatting using Intl.NumberFormat + - `palette.ts` - Category color mapping and labels (internationalized) + - `utils.ts` - General utilities +- Depends on: Supabase SDK, TypeScript types +- Used by: Hooks and components for types and helpers + +**Authentication Layer:** +- Purpose: Manage session state and auth operations +- Location: `src/hooks/useAuth.ts` +- Implementation: Reactive Supabase auth listener with automatic session refresh +- State: Session, user, loading flag +- Operations: signUp, signIn, signInWithOAuth (Google, GitHub), signOut + +**Internationalization (i18n):** +- Purpose: Provide multi-language support at runtime +- Location: `src/i18n/index.ts` with JSON resource files (`en.json`, `de.json`) +- Framework: react-i18next with i18next +- Initialization: Automatic at app startup before any render + +## Data Flow + +**Read Pattern (Fetch & Display):** + +1. Component renders and mounts +2. Component calls custom hook (e.g., `useBudgets()`, `useCategories()`) +3. Hook initializes TanStack Query with async queryFn +4. Query function calls Supabase table select with filters/joins +5. Supabase returns typed rows from database +6. Hook caches result in Query store with staleTime of 5 minutes +7. Component receives `data`, `loading`, and error states +8. Component renders based on data state +9. Subsequent mounts of same component use cached data (until stale) + +**Write Pattern (Mutate & Sync):** + +1. Component invokes mutation handler (e.g., click "Save") +2. Handler calls `mutation.mutateAsync(payload)` +3. Mutation function marshals payload and calls Supabase insert/update/delete +4. Supabase executes DB operation and returns modified row(s) +5. Mutation onSuccess callback triggers Query invalidation +6. Query re-fetches from server with fresh data +7. Component re-renders with new cached data +8. Toast notification indicates success or error + +**Real-time Budget Updates:** + +Example flow for editing a budget item (from `useBudgets.ts`): +1. User edits amount in budget detail table → calls `updateItem.mutateAsync({ id, budgetId, budgeted_amount: X })` +2. Hook serializes to Supabase `.update()` with .eq("id", id) +3. Response contains updated BudgetItem with joined Category data +4. onSuccess invalidates `["budgets", budgetId, "items"]` cache key +5. DashboardContent's useBudgetDetail query re-fetches entire items array +6. Component recalculates totals and re-renders pie chart and progress bars + +**State Management:** +- No Redux or Zustand — TanStack Query handles async state +- Local component state for UI interactions (dialogs, forms, selections) +- Session state maintained by Supabase auth listener in useAuth hook +- Cache invalidation is the primary sync mechanism + +## Key Abstractions + +**Query Key Pattern:** + +Each hook defines typed query keys as const arrays: +- `["categories"]` - all user categories +- `["budgets"]` - all user budgets +- `["budgets", id]` - single budget +- `["budgets", id, "items"]` - items for a specific budget +- `["template"]` - user's monthly template +- `["template-items"]` - items in template +- `["quick-add"]` - user's quick-add library + +Purpose: Enables granular invalidation (e.g., update one budget doesn't refetch all budgets) + +**Mutation Factories:** + +Hooks expose mutations as properties of returned object: +- `useCategories()` returns `{ categories, loading, create, update, remove }` +- `useBudgets()` returns `{ budgets, loading, createBudget, generateFromTemplate, updateItem, createItem, deleteItem, deleteBudget }` + +Each mutation includes: +- `mutationFn` - async operation against Supabase +- `onSuccess` - cache invalidation strategy + +**Hook Composition:** + +`useBudgetDetail(id)` is both a standalone export and accessible via `useBudgets().getBudget(id)`: +- Enables flexible usage patterns +- Query with `.enabled: Boolean(id)` prevents queries when id is falsy +- Used by DashboardPage to fetch current month's budget data + +**Type Hierarchy:** + +Core types in `src/lib/types.ts`: +- `Profile` - user profile with locale and currency +- `Category` - user-defined expense category +- `Template` & `TemplateItem` - monthly budget template (fixed/variable items) +- `Budget` & `BudgetItem` - actual budget with tracked actual/budgeted amounts +- `QuickAddItem` - quick entry library for rapid data entry + +Relationships: +- User → many Profiles (one per row, per user) +- User → many Categories +- User → one Template (auto-created) +- Template → many TemplateItems → Category (join) +- User → many Budgets (monthly) +- Budget → many BudgetItems → Category (join) +- User → many QuickAddItems + +## Entry Points + +**Application Root:** +- Location: `src/main.tsx` +- Initializes React 19 with context providers: + - QueryClientProvider (TanStack Query) + - BrowserRouter (React Router) + - TooltipProvider (Radix UI) + - Toaster (Sonner notifications) +- Creates single QueryClient with 5-minute staleTime default + +**Route Configuration:** +- Location: `src/App.tsx` +- Defines protected and public routes +- Protected routes wrapped in `ProtectedRoute` which checks auth state via `useAuth()` +- Public routes wrapped in `PublicRoute` which redirects authenticated users away +- Routes: + - `/login`, `/register` - public (redirect home if logged in) + - `/`, `/categories`, `/template`, `/budgets`, `/budgets/:id`, `/quick-add`, `/settings` - protected + +**Layout Root:** +- Location: `src/components/AppLayout.tsx` +- Renders Sidebar with navigation items (using Radix UI primitives) +- Renders Outlet for nested route content +- Provides sign-out button in footer + +**Page Entry Points:** + +Each page component is stateless renderer with logic split between: +1. Page component (routing layer, layout) +2. Sub-component(s) (content logic) +3. Hook(s) (data fetching) + +Example: `DashboardPage.tsx` → +- Main component finds current month's budget +- Delegates to `DashboardContent` with `budgetId` prop +- DashboardContent calls `useBudgetDetail(budgetId)` for data + +## Error Handling + +**Strategy:** Try-catch in mutation handlers with toast notifications for user feedback. + +**Patterns:** + +1. **Mutation Errors:** + - Try block executes `mutation.mutateAsync()` + - Catch block logs to console and shows `toast.error(t("common.error"))` + - Button remains clickable if error is transient + - Example in `TemplatePage.tsx` lines 182-204 + +2. **Query Errors:** + - Query errors are captured by TanStack Query internally + - Hooks return loading state but not explicit error state + - Pages render null/loading state during failed queries + - Retry is configured to attempt once by default + +3. **Auth Errors:** + - `useAuth.ts` catches Supabase auth errors and re-throws + - LoginPage catches and displays error message in red text + - Session state remains null if auth fails + +4. **Missing Environment Variables:** + - `src/lib/supabase.ts` throws during module load if VITE_SUPABASE_URL or VITE_SUPABASE_ANON_KEY missing + - Prevents runtime errors from undefined client + +## Cross-Cutting Concerns + +**Logging:** +Minimal logging — relies on: +- Browser DevTools React Query Devtools (installable) +- Console errors from try-catch blocks +- Sonner toast notifications for user-facing issues + +**Validation:** +- Input validation in mutation handlers (e.g., category ID check, amount > 0) +- Type validation via TypeScript compile-time +- Supabase schema constraints (NOT NULL, FOREIGN KEY, CHECK constraints) +- Database uniqueness constraints (e.g., one template per user) + +**Authentication:** +- Supabase session token stored in localStorage (browser SDK default) +- Auth state checked at page render time via `useAuth()` hook +- Protected routes redirect unauthenticated users to /login +- Session persists across page refreshes (Supabase handles recovery) + +**Authorization:** +- Row-level security (RLS) policies on Supabase tables enforce user_id filters +- No explicit authorization logic in client (relies on DB policies) +- All queries filter by current user via `await supabase.auth.getUser()` + +--- + +*Architecture analysis: 2026-03-16* diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md new file mode 100644 index 0000000..7a8fcaa --- /dev/null +++ b/.planning/codebase/CONCERNS.md @@ -0,0 +1,224 @@ +# Codebase Concerns + +**Analysis Date:** 2026-03-16 + +## Tech Debt + +**Unsafe Type Assertions:** +- Issue: Environment variables are cast with `as string` without runtime validation +- Files: `src/lib/supabase.ts` +- Impact: If environment variables are missing or undefined, the application will fail at runtime with cryptic errors instead of clear validation +- Fix approach: Implement proper environment variable validation at startup with descriptive error messages. Use a validation function or schema validator (e.g., Zod) to ensure all required env vars are present and properly typed before using them. + +**Unvalidated Supabase Query Results:** +- Issue: Database query results are cast to types with `as Type` without validation (e.g., `data as Budget[]`, `data as Category[]`) +- Files: `src/hooks/useBudgets.ts`, `src/hooks/useTemplate.ts`, `src/hooks/useCategories.ts`, `src/hooks/useQuickAdd.ts` +- Impact: If database schema changes or returns unexpected data structure, application could crash silently or display incorrect data. Type casting bypasses TypeScript safety. +- Fix approach: Add runtime validation using Zod or similar schema validation library. Define schemas for all database types and validate responses before casting. + +**Hardcoded Date Logic Without Timezone Handling:** +- Issue: Date calculations in `monthBounds()` and budget queries use local timezone without explicit handling +- Files: `src/hooks/useBudgets.ts` (line 27-42), `src/pages/DashboardPage.tsx` (line 37-39) +- Impact: Users in different timezones may see incorrect month boundaries. Budget dates could be off by one day depending on user timezone. +- Fix approach: Use a date library like `date-fns` or `Day.js` with explicit timezone support. Store all dates in UTC and format for display based on user locale. + +**No Error Boundary Component:** +- Issue: Application has no React Error Boundary to catch and handle rendering errors gracefully +- Files: `src/App.tsx`, `src/components/AppLayout.tsx` +- Impact: A single component error can crash the entire application without user feedback. No recovery mechanism. +- Fix approach: Implement an Error Boundary wrapper at the root level and key sections to gracefully display fallback UI and log errors. + +## Known Bugs + +**Query Invalidation Race Conditions:** +- Symptoms: When mutations complete, related queries are invalidated but may not refetch before components re-render +- Files: `src/hooks/useBudgets.ts`, `src/hooks/useTemplate.ts`, `src/hooks/useCategories.ts` +- Trigger: Rapid mutations on related data (e.g., updating template items then immediately viewing budget detail) +- Workaround: Manually refetch or await invalidation before navigation + +**Logout Doesn't Clear Cache:** +- Symptoms: After logout, if user logs back in as different user, old data may still be visible momentarily +- Files: `src/hooks/useAuth.ts`, `src/components/AppLayout.tsx` +- Trigger: User logs out, then logs in as different account +- Workaround: Clear QueryClient cache on logout + +**Missing Bounds Check on Inline Edits:** +- Symptoms: User can enter negative numbers or extremely large numbers in inline edit cells +- Files: `src/pages/BudgetDetailPage.tsx` (InlineEditCell component) +- Trigger: User enters invalid amount in inline editor +- Workaround: Client-side validation only; no server-side constraints shown + +## Security Considerations + +**Unauthenticated Supabase Client Exposed:** +- Risk: Application uses public anon key for Supabase, all clients share same authentication +- Files: `src/lib/supabase.ts` +- Current mitigation: Supabase RLS policies on database tables +- Recommendations: Verify RLS policies are correctly set on ALL tables. Test that users cannot access other users' data through direct API calls. Consider using service role key for sensitive operations and server-side validation. + +**No Input Validation on User-Provided Data:** +- Risk: Category names, template names, quick-add names accepted without validation or sanitization +- Files: `src/pages/CategoriesPage.tsx`, `src/pages/TemplatePage.tsx`, `src/pages/QuickAddPage.tsx`, `src/components/QuickAddPicker.tsx` +- Current mitigation: Input length limited by UI +- Recommendations: Add server-side validation for text length, character restrictions, and XSS prevention. Sanitize data before display. + +**No Rate Limiting on Mutations:** +- Risk: User can spam API with unlimited mutations (create, update, delete operations) +- Files: All hook files with mutations +- Current mitigation: None +- Recommendations: Implement client-side debouncing and server-side rate limiting per user. Add confirmation dialogs for destructive operations. + +**Cleartext Storage of Sensitive Data:** +- Risk: Notes field on budget items can contain sensitive information but has no encryption +- Files: `src/pages/BudgetDetailPage.tsx` (notes field) +- Current mitigation: None +- Recommendations: Add encryption for notes field or implement field-level access control via RLS. + +## Performance Bottlenecks + +**Inefficient Pie Chart Rendering on Large Budgets:** +- Problem: PieChart component recalculates and re-renders on every data change without memoization +- Files: `src/pages/DashboardPage.tsx` (lines 104-109, Pie chart rendering) +- Cause: Pie chart data transformation happens during render, component not memoized +- Improvement path: Memoize chart data calculations with `useMemo()`. Consider moving heavy computations outside render. + +**Category Lookup O(n) on Every Item:** +- Problem: Every budget item with category data requires iterating through categories array in filters +- Files: `src/pages/DashboardPage.tsx`, `src/pages/BudgetDetailPage.tsx` +- Cause: Using `filter()` and `reduce()` in render logic without memoization or indexing +- Improvement path: Create a category index Map in a custom hook. Memoize grouped/filtered results. + +**No Pagination on Long Lists:** +- Problem: All budgets, categories, and quick-add items load at once. No pagination or virtualization. +- Files: `src/pages/BudgetListPage.tsx`, `src/pages/CategoriesPage.tsx`, `src/pages/QuickAddPage.tsx` +- Cause: Supabase queries fetch all rows without limit +- Improvement path: Implement pagination with `.range()` in queries. For very large lists, use virtual scrolling. + +**Sidebar Component Overcomplicated:** +- Problem: Sidebar UI component is 724 lines with extensive state and conditional logic +- Files: `src/components/ui/sidebar.tsx` +- Cause: Radix UI base component with full feature set +- Improvement path: Extract mobile/desktop logic into separate custom hooks. Consider if full sidebar complexity is needed. + +## Fragile Areas + +**Budget Month Lookup Logic:** +- Files: `src/pages/DashboardPage.tsx` (lines 285-289), `src/hooks/useBudgets.ts` (line 28-32) +- Why fragile: String prefix matching on dates to find current month budget. If date format changes, breaks silently. +- Safe modification: Create dedicated date utility functions with tests. Use date library comparison instead of string prefixes. +- Test coverage: No unit tests for monthBounds or month lookup logic + +**QuickAddPicker Multi-Dialog State:** +- Files: `src/components/QuickAddPicker.tsx` +- Why fragile: Manages two modal states (popover + dialog) with multiple interdependent state variables. Easy to get out of sync. +- Safe modification: Refactor into a state machine pattern or context provider. Create helper function to reset all state. +- Test coverage: No tests for state transitions or edge cases (e.g., closing popover while dialog open) + +**Inline Edit Cell Implementation:** +- Files: `src/pages/BudgetDetailPage.tsx` (lines 68-133) +- Why fragile: `useRef` with imperative focus, async commit logic could leave cell in inconsistent state on error +- Safe modification: Add error handling in commit() to reset editing state. Use useCallback to memoize handlers. +- Test coverage: No tests for edit flow, cancellation, or blur behavior + +**Template Item Reordering:** +- Files: `src/hooks/useTemplate.ts` (lines 174-189) +- Why fragile: Uses Promise.all for batch updates, first error stops entire operation but leaves partial state +- Safe modification: Implement transaction-like behavior or rollback on error. Show partial success/failure feedback. +- Test coverage: No tests for concurrent updates or error scenarios + +**Date Parsing in Budget Heading:** +- Files: `src/pages/BudgetDetailPage.tsx` (lines 275-280) +- Why fragile: Splits date string and uses `map(Number)` which could silently fail if date format changes +- Safe modification: Use date parsing library. Add validation for date format. +- Test coverage: No tests for date format parsing + +## Scaling Limits + +**QueryClient Cache Without Limits:** +- Current capacity: Unbounded cache growth with every mutation +- Limit: Long sessions could consume significant memory with stale cached data +- Scaling path: Implement QueryClient cache configuration with GC time, stale time, and maximum cache size + +**No Database Indexing Strategy Documented:** +- Current capacity: Unknown if queries will scale beyond thousands of items +- Limit: Performance degradation as user data grows +- Scaling path: Add database indexes on user_id, category_id, budget_id fields. Document query patterns. + +**Sidebar Component Renders Full Navigation on Every App Load:** +- Current capacity: Works fine with <10 navigation items +- Limit: Could become slow if navigation items scale to hundreds +- Scaling path: Implement menu virtualization or lazy loading for navigation items + +## Dependencies at Risk + +**React Query (TanStack Query) Version Lock:** +- Risk: Fixed to v5.90.21, major version bumps require API migration +- Impact: Security fixes in newer versions require full refactor +- Migration plan: Create abstraction layer for React Query hooks to isolate API. Plan quarterly dependency updates. + +**Supabase SDK Not Version-Locked:** +- Risk: Using ^2.99.1 allows minor/patch updates that could introduce breaking changes +- Impact: Unexpected behavior in authentication or database operations +- Migration plan: Pin to exact version during development, test thoroughly before minor version upgrades + +**Recharts for Charts:** +- Risk: Limited customization without ejecting to custom D3 +- Impact: Cannot easily implement complex financial chart types needed in future +- Migration plan: Consider migrating to Chart.js or Visx if advanced charting requirements emerge + +## Missing Critical Features + +**No Data Export:** +- Problem: Users cannot export budgets or transaction data +- Blocks: Users cannot do external analysis, tax reporting, or data portability + +**No Recurring Transactions:** +- Problem: Each month requires manual budget recreation or template generation +- Blocks: Can't model monthly recurring expenses that auto-populate + +**No Budget Archival:** +- Problem: Old budgets accumulate in database, no way to hide or delete safely +- Blocks: UI becomes cluttered, performance degrades + +**No Audit Trail:** +- Problem: No tracking of who changed what and when +- Blocks: Cannot debug data inconsistencies or provide accountability + +**No Multi-Device Sync:** +- Problem: Offline support or sync conflicts not handled +- Blocks: Mobile app development would be difficult + +## Test Coverage Gaps + +**No Unit Tests:** +- What's not tested: All business logic, date calculations, budget math, filter/reduce operations +- Files: `src/hooks/`, `src/lib/` +- Risk: Regression in calculation logic could go unnoticed (e.g., budget overage math, currency rounding) +- Priority: High - calculation errors directly impact financial data + +**No Integration Tests:** +- What's not tested: Mutation sequences, error recovery, cache invalidation, query dependencies +- Files: All pages that use multiple mutations +- Risk: Race conditions and inconsistent state when multiple operations happen quickly +- Priority: High - production data corruption risk + +**No Component Tests:** +- What's not tested: Inline edit cells, modal state management, form validation, error states +- Files: `src/pages/BudgetDetailPage.tsx`, `src/components/QuickAddPicker.tsx` +- Risk: UI behavior breaks silently (e.g., edit not saving, delete confirmation not working) +- Priority: Medium - affects user experience + +**No E2E Tests:** +- What's not tested: Complete user workflows (login → create budget → add items → view dashboard) +- Risk: Critical paths fail only in production +- Priority: Medium - would catch integration failures early + +**No Error Path Testing:** +- What's not tested: Network errors, auth failures, database errors, invalid data +- Files: All mutation handlers use generic toast.error() without specific handling +- Risk: Users see unhelpful error messages, cannot recover gracefully +- Priority: Medium - impacts user experience and debugging + +--- + +*Concerns audit: 2026-03-16* diff --git a/.planning/codebase/CONVENTIONS.md b/.planning/codebase/CONVENTIONS.md new file mode 100644 index 0000000..3095c2a --- /dev/null +++ b/.planning/codebase/CONVENTIONS.md @@ -0,0 +1,257 @@ +# Coding Conventions + +**Analysis Date:** 2026-03-16 + +## Naming Patterns + +**Files:** +- React components: PascalCase with .tsx extension - `QuickAddPicker.tsx`, `LoginPage.tsx` +- Custom hooks: camelCase starting with "use" with .ts extension - `useAuth.ts`, `useCategories.ts`, `useBudgets.ts` +- Utilities and type files: camelCase with .ts extension - `utils.ts`, `types.ts`, `format.ts`, `palette.ts` +- UI components: lowercase with hyphens for compound names - `button.tsx`, `dropdown-menu.tsx`, `alert-dialog.tsx` +- Directories: lowercase with hyphens for multi-word names - `components/ui`, `pages`, `hooks`, `lib` + +**Functions:** +- React components: PascalCase - `QuickAddPicker`, `LoginPage`, `AppLayout` +- Hook functions: camelCase with "use" prefix - `useAuth()`, `useIsMobile()`, `useBudgets()` +- Utility functions: camelCase - `cn()`, `formatCurrency()`, `monthBounds()` +- Event handlers: camelCase starting with "handle" - `handlePickItem()`, `handleSave()`, `handleDialogClose()` +- Private/internal helpers: lowercase with underscore prefix when needed or nested as sub-functions + +**Variables:** +- State variables: camelCase - `session`, `user`, `loading`, `popoverOpen`, `selectedItem` +- Constants: UPPER_SNAKE_CASE - `MOBILE_BREAKPOINT`, `CATEGORY_TYPES`, `BUDGETS_KEY` +- Query keys: lowercase with underscores - `budgets`, `categories`, `templates` +- Boolean variables: descriptive names - `isMobile`, `canSave`, `loading`, `isLoading` + +**Types:** +- Interfaces: PascalCase - `Profile`, `Category`, `Budget`, `BudgetItem` +- Type unions: PascalCase or vertical bar notation - `type CategoryType = "income" | "bill" | ...` +- Generic parameters: single uppercase letter or descriptive PascalCase - `T`, `Data` + +## Code Style + +**Formatting:** +- No explicit formatter configured, but code follows consistent patterns +- 2-space indentation (standard TypeScript/JavaScript practice) +- Multiline imports organized by source type +- Trailing commas in multiline arrays and objects +- Semicolons at end of statements + +**Linting:** +- Tool: ESLint (version 9.39.4) with Flat Config +- Config: `eslint.config.js` +- Extends: `@eslint/js`, `typescript-eslint`, `eslint-plugin-react-hooks`, `eslint-plugin-react-refresh` +- Key rules enforced: + - React hooks best practices (`react-hooks/rules-of-hooks`) + - React refresh compatibility checks + - TypeScript recommended rules + +## Import Organization + +**Order:** +1. React imports and hooks - `import { useState } from "react"` +2. Third-party libraries - `import { useQuery } from "@tanstack/react-query"` +3. Supabase imports - `import { supabase } from "@/lib/supabase"` +4. Internal types - `import type { Category } from "@/lib/types"` +5. Internal utilities - `import { cn } from "@/lib/utils"` +6. Components - `import { Button } from "@/components/ui/button"` +7. Other imports - hooks, constants, etc. + +**Path Aliases:** +- `@/*` resolves to `./src/*` (configured in `tsconfig.app.json`) +- Relative imports used only for co-located files +- All absolute imports use the `@/` prefix + +**Example pattern from `QuickAddPicker.tsx`:** +```typescript +import { useState } from "react" +import { useTranslation } from "react-i18next" +import { Zap } from "lucide-react" +import { toast } from "sonner" +import { useQuickAdd } from "@/hooks/useQuickAdd" +import { useCategories } from "@/hooks/useCategories" +import { useBudgets } from "@/hooks/useBudgets" +import type { QuickAddItem, CategoryType } from "@/lib/types" +import { categoryColors } from "@/lib/palette" +import { Button } from "@/components/ui/button" +``` + +## Error Handling + +**Patterns:** +- Supabase errors checked with `if (error) throw error` pattern +- Async/await used for promises with try/catch at caller level +- User-facing errors converted to toast notifications using `sonner` +- Authentication errors throw and are caught in component try/catch blocks +- Validation errors prevented via state checks before action (`canSave` boolean pattern) + +**Example from `useCategories.ts`:** +```typescript +const { data, error } = await supabase.from("categories").select("*") +if (error) throw error +return data as Category[] +``` + +**Example from `LoginPage.tsx`:** +```typescript +try { + await signIn(email, password) + navigate("/") +} catch (err) { + setError(err instanceof Error ? err.message : t("common.error")) +} +``` + +## Logging + +**Framework:** No structured logging library. Uses standard browser console methods implicitly. + +**Patterns:** +- No verbose console.log calls in code +- Relies on error boundaries and try/catch for debugging +- Toast notifications (via `sonner`) for user feedback on async operations +- Translation keys used for all user-facing messages + +**Example from `QuickAddPicker.tsx`:** +```typescript +catch { + toast.error(t("common.error")) +} +``` + +## Comments + +**When to Comment:** +- Complex business logic documented with block comments +- Multi-step mutations explained with numbered sections +- Props documented with JSDoc-style comments on interfaces +- Section separators used to organize large components (see 80+ line files) + +**JSDoc/TSDoc:** +- Used on exported functions and interfaces +- Describes purpose, parameters, and return values +- Example from `useBudgets.ts`: +```typescript +/** + * Given a 1-based month and a full year, return ISO date strings for the + * first and last day of that month. + */ +function monthBounds( + month: number, + year: number +): { start_date: string; end_date: string } +``` + +**Documentation Comments on Props:** +```typescript +interface QuickAddPickerProps { + /** The id of the current month's budget to add the item to. */ + budgetId: string +} +``` + +## Function Design + +**Size:** Functions kept under 100 lines; larger components organized with comment sections + +**Parameters:** +- Single simple types preferred +- Object destructuring for multiple related parameters +- Type annotations always present for parameters +- Optional parameters marked with `?:` + +**Return Values:** +- Hooks return objects with destructurable properties +- Components return JSX.Element +- Explicit return type annotations on typed functions +- null/undefined handled explicitly in return statements + +**Example pattern from `useBudgets.ts`:** +```typescript +return { + budgets: budgetsQuery.data ?? [], + loading: budgetsQuery.isLoading, + getBudget, + createBudget, + generateFromTemplate, + updateItem, + createItem, + deleteItem, + deleteBudget, +} +``` + +## Module Design + +**Exports:** +- Named exports for utility functions - `export function cn(...)` +- Default exports for React components - `export default function QuickAddPicker` +- Type-only exports for TypeScript types - `export type CategoryType = ...` +- Multiple related hooks exported from single file - `useBudgets()` and `useBudgetDetail()` from same file + +**Barrel Files:** +- Not used; imports reference specific files directly +- Example: `import { Button } from "@/components/ui/button"` not from `@/components/ui` + +**File Organization:** +- One main export per file +- Related utilities and helpers in same file with clear section comments +- Query key constants defined at top of custom hooks before the hook itself + +## TypeScript Configuration + +**Strict Mode:** Enabled (`"strict": true`) +- All implicit `any` types caught +- Null and undefined checking enforced +- Type assertions allowed but discouraged + +**Key Options (`tsconfig.app.json`):** +- Target: ES2023 +- Module: ESNext +- JSX: react-jsx (React 17+ transform) +- Strict mode enabled +- `noUnusedLocals` and `noUnusedParameters` enforced +- Path aliases configured for `@/*` + +## React Patterns + +**Hooks:** +- `useState` for local component state +- `useEffect` for side effects with proper cleanup +- Custom hooks for data fetching and logic reuse +- `useTranslation` from react-i18next for i18n +- `useQueryClient` from @tanstack/react-query for cache invalidation + +**Component Structure:** +- Functional components only +- Props typed with TypeScript interfaces +- Event handlers defined as functions (not inline) +- Section comments separate concerns (Constants, Props, Render) + +**Forms:** +- Controlled inputs with onChange handlers +- Form submission prevents default with `e.preventDefault()` +- Validation done before mutation execution +- Error state displayed to user via toast or error field + +## Data Validation + +**Approach:** Type safety first with TypeScript +- All data from Supabase cast with `as Type` after type-safe query +- Input validation in component state checks before action +- Zod not used; relies on TypeScript types and Supabase schema +- Client-side checks prevent invalid mutations from firing + +**Example from `QuickAddPicker.tsx`:** +```typescript +const canSave = + Boolean(categoryId) && + Boolean(amount) && + !isNaN(parseFloat(amount)) && + parseFloat(amount) >= 0 +``` + +--- + +*Convention analysis: 2026-03-16* diff --git a/.planning/codebase/INTEGRATIONS.md b/.planning/codebase/INTEGRATIONS.md new file mode 100644 index 0000000..ff15630 --- /dev/null +++ b/.planning/codebase/INTEGRATIONS.md @@ -0,0 +1,124 @@ +# External Integrations + +**Analysis Date:** 2026-03-16 + +## APIs & External Services + +**Supabase Backend:** +- Supabase - Primary backend-as-a-service platform + - SDK/Client: `@supabase/supabase-js` 2.99.1 + - Auth: Environment variables `VITE_SUPABASE_URL` and `VITE_SUPABASE_ANON_KEY` + - Client initialization: `src/lib/supabase.ts` + +## Data Storage + +**Databases:** +- PostgreSQL (Supabase hosted) + - Connection: Via `supabase` client in `src/lib/supabase.ts` + - Client: Supabase JavaScript SDK + - Tables: profiles, categories, templates, budgets, quick_add + - Row-level security (RLS) enabled on all user data tables + - Auto-trigger on signup: `handle_new_user()` creates user profile + +**Migrations:** +- Location: `supabase/migrations/` +- `001_profiles.sql` - User profiles with display name, locale, currency preferences +- `002_categories.sql` - Transaction category definitions +- `003_templates.sql` - Expense templates +- `004_budgets.sql` - Budget management +- `005_quick_add.sql` - Quick transaction templates + +**File Storage:** +- Not detected (no file upload functionality) + +**Caching:** +- React Query client-side caching + - Stale time: 5 minutes for queries + - Retry: 1 attempt on failure + - Configuration: `src/main.tsx` + +## Authentication & Identity + +**Auth Provider:** +- Supabase Authentication + - Implementation: Email/password and OAuth (Google, GitHub) + - Hook: `src/hooks/useAuth.ts` + - Methods: + - `signUp(email, password)` - Email registration + - `signIn(email, password)` - Email login + - `signInWithOAuth(provider)` - OAuth providers (google, github) + - `signOut()` - Sign out and session cleanup + - Session management: Automatic via `onAuthStateChange` listener + - State storage: React hooks (session, user, loading states) + +## Monitoring & Observability + +**Error Tracking:** +- Not detected + +**Logs:** +- Browser console logging only +- Error propagation via toast notifications (Sonner library) + +## CI/CD & Deployment + +**Hosting:** +- Not detected (SPA intended for static hosting) + +**CI Pipeline:** +- Not detected + +## Environment Configuration + +**Required env vars:** +- `VITE_SUPABASE_URL` - Supabase project URL +- `VITE_SUPABASE_ANON_KEY` - Supabase anonymous/public key +- Both are validated at client initialization in `src/lib/supabase.ts` +- Missing values throw error: "Missing VITE_SUPABASE_URL or VITE_SUPABASE_ANON_KEY env vars" + +**Secrets location:** +- `.env` file (local, not committed) +- Example template: `.env.example` (with placeholder values) + +## Webhooks & Callbacks + +**Incoming:** +- Supabase OAuth redirect callbacks (Google, GitHub) +- Handled by Supabase SDK automatically + +**Outgoing:** +- Not detected + +## API Client Hooks + +**Data Fetching:** +- `src/hooks/useAuth.ts` - Authentication state and session management +- `src/hooks/useCategories.ts` - Category CRUD operations via React Query +- `src/hooks/useTemplate.ts` - Template CRUD operations via React Query +- `src/hooks/useBudgets.ts` - Budget CRUD operations with detail view support +- `src/hooks/useQuickAdd.ts` - Quick add items management via React Query + +All hooks use TanStack React Query for: +- Server state management +- Automatic caching +- Background refetching +- Mutation handling (create, update, delete) +- Query client invalidation for consistency + +## Database Access Pattern + +**Row Level Security:** +- All tables use RLS policies to restrict access to authenticated users +- Users can only read/write their own data via `auth.uid()` checks +- Policies enforced at database level for security + +**Data Relationships:** +- `profiles` (user data) ← extends `auth.users` +- `categories` (user expense categories) +- `templates` (saved expense templates) +- `budgets` (budget tracking with items) +- `quick_add` (quick transaction presets) + +--- + +*Integration audit: 2026-03-16* diff --git a/.planning/codebase/STACK.md b/.planning/codebase/STACK.md new file mode 100644 index 0000000..b8dafce --- /dev/null +++ b/.planning/codebase/STACK.md @@ -0,0 +1,118 @@ +# Technology Stack + +**Analysis Date:** 2026-03-16 + +## Languages + +**Primary:** +- TypeScript ~5.9.3 - Full codebase type safety + +**Secondary:** +- JavaScript (ES2023) - Configuration and utilities + +## Runtime + +**Environment:** +- Browser (React 19 SPA) +- Node.js runtime for build tooling + +**Package Manager:** +- Bun - Package and dependency management +- Lockfile: `bun.lock` (present) + +## Frameworks + +**Core:** +- React 19.2.4 - UI component framework +- React Router DOM 7.13.1 - Client-side routing + +**UI & Components:** +- Radix UI 1.4.3 - Accessible component primitives +- Tailwind CSS 4.2.1 - Utility-first CSS styling +- Lucide React 0.577.0 - Icon library +- Sonner 2.0.7 - Toast notification system +- next-themes 0.4.6 - Dark mode and theme management + +**State Management:** +- TanStack React Query 5.90.21 - Server state management and data fetching + +**Internationalization:** +- i18next 25.8.18 - i18n framework +- react-i18next 16.5.8 - React bindings for i18next + +**Testing:** +- Not detected + +**Build/Dev:** +- Vite 8.0.0 - Build tool and dev server +- @vitejs/plugin-react 6.0.0 - React Fast Refresh support +- @tailwindcss/vite 4.2.1 - Tailwind CSS Vite plugin +- Tailwind Merge 3.5.0 - CSS class utility merging +- Class Variance Authority 0.7.1 - CSS variant composition +- clsx 2.1.1 - Conditional CSS class binding + +**Linting & Code Quality:** +- ESLint 9.39.4 - JavaScript/TypeScript linting +- @eslint/js 9.39.4 - ESLint JS config +- typescript-eslint 8.56.1 - TypeScript linting rules +- eslint-plugin-react-hooks 7.0.1 - React Hooks best practices +- eslint-plugin-react-refresh 0.5.2 - React Fast Refresh plugin + +## Key Dependencies + +**Critical:** +- @supabase/supabase-js 2.99.1 - Backend API and authentication client + +**Infrastructure:** +- @types/node 24.12.0 - TypeScript Node.js type definitions +- @types/react 19.2.14 - React TypeScript types +- @types/react-dom 19.2.3 - React DOM TypeScript types +- globals 17.4.0 - Global scope definitions for ESLint + +## Configuration + +**Environment:** +- Vite environment variables with `VITE_` prefix +- Required env vars: `VITE_SUPABASE_URL`, `VITE_SUPABASE_ANON_KEY` +- Configuration via `.env` file (example provided in `.env.example`) + +**Build:** +- Vite config: `vite.config.ts` + - React plugin enabled + - Tailwind CSS via @tailwindcss/vite + - Path alias: `@/` resolves to `./src/` +- TypeScript config: `tsconfig.json` (references `tsconfig.app.json` and `tsconfig.node.json`) + - Target: ES2023 + - Strict mode enabled + - No unused locals/parameters enforcement +- ESLint config: `eslint.config.js` + - Flat config format + - Recommended configs: JS, TypeScript, React Hooks, React Refresh + - Browser globals enabled + +## TypeScript Configuration + +**tsconfig.app.json:** +- Target: ES2023 +- Module: ESNext +- Strict mode: Enabled +- JSX: react-jsx +- No emit: True (code generation disabled) +- Path alias: `@/*` → `./src/*` +- Verbatim module syntax enabled + +## Platform Requirements + +**Development:** +- Node.js/Bun runtime +- Modern browser with ES2023 support +- Recommended: 18+ GB disk for `node_modules` + +**Production:** +- SPA deployment (static hosting) +- Supabase PostgreSQL backend access +- Modern browser JavaScript support (ES2023) + +--- + +*Stack analysis: 2026-03-16* diff --git a/.planning/codebase/STRUCTURE.md b/.planning/codebase/STRUCTURE.md new file mode 100644 index 0000000..45a263f --- /dev/null +++ b/.planning/codebase/STRUCTURE.md @@ -0,0 +1,249 @@ +# Codebase Structure + +**Analysis Date:** 2026-03-16 + +## Directory Layout + +``` +SimpleFinanceDash/ +├── src/ # All application source code +│ ├── main.tsx # React 19 app entry point with providers +│ ├── App.tsx # Route configuration and guards +│ ├── index.css # Global Tailwind styles +│ ├── pages/ # Page components (routed views) +│ ├── components/ # Reusable and layout components +│ │ └── ui/ # Shadcn/Radix UI primitives (16 components) +│ ├── hooks/ # Custom data-fetching hooks (TanStack Query) +│ ├── lib/ # Utilities, types, configuration +│ └── i18n/ # Internationalization setup and resources +├── supabase/ # Supabase project files +│ └── migrations/ # Database schema migrations +├── public/ # Static assets +├── index.html # HTML entry point for Vite +├── package.json # Dependencies and build scripts +├── vite.config.ts # Vite bundler configuration +├── tsconfig.json # TypeScript base configuration +├── tsconfig.app.json # App-specific TypeScript config +├── tsconfig.node.json # Build script TypeScript config +├── eslint.config.js # ESLint configuration +├── components.json # Shadcn CLI component registry +├── .env.example # Example environment variables template +└── .gitignore # Git ignore rules + +``` + +## Directory Purposes + +**`src/pages/`:** +- Purpose: Page-level components that correspond to routes in App.tsx +- Contains: 9 page components (all .tsx) +- Key files: + - `DashboardPage.tsx` (316 lines) - Monthly budget dashboard with charts + - `BudgetDetailPage.tsx` (555 lines) - Detailed budget editing and item management + - `TemplatePage.tsx` (459 lines) - Monthly budget template editor + - `BudgetListPage.tsx` (261 lines) - List of all budgets with quick actions + - `CategoriesPage.tsx` (214 lines) - Category management (CRUD) + - `QuickAddPage.tsx` (202 lines) - Quick-add library editor + - `SettingsPage.tsx` (125 lines) - User preferences + - `LoginPage.tsx` (107 lines) - Email/password and OAuth login + - `RegisterPage.tsx` (81 lines) - User registration form + +Each page follows pattern: +- Imports hooks at top +- Calls hooks in component body +- Renders UI from state +- Delegates complex logic to sub-components + +**`src/components/`:** +- Purpose: Reusable UI components and layout wrappers +- Contains: Custom components + UI library primitives +- Key files: + - `AppLayout.tsx` - Sidebar wrapper for authenticated pages + - `QuickAddPicker.tsx` - Multi-modal quick-add workflow component + - `ui/` - 16 Shadcn-based components (Button, Dialog, Select, Table, etc.) + +**`src/hooks/`:** +- Purpose: Encapsulate all server communication and state queries +- Contains: 6 custom hooks +- Key files: + - `useAuth.ts` - Session management and auth operations (signUp, signIn, signOut) + - `useBudgets.ts` - Budget CRUD, item management, template generation + - `useCategories.ts` - Category CRUD operations + - `useTemplate.ts` - Monthly budget template management + - `useQuickAdd.ts` - Quick-add item library CRUD + - `use-mobile.ts` - Responsive breakpoint detection utility + +Each hook: +- Defines typed query keys as const arrays +- Initializes useQuery/useMutation from TanStack Query +- Returns { data, loading, ...mutations } +- Implements onSuccess cache invalidation + +**`src/lib/`:** +- Purpose: Core utilities, types, and configuration +- Contains: 5 files +- Key files: + - `types.ts` - TypeScript interfaces: Profile, Category, Budget, BudgetItem, Template, TemplateItem, QuickAddItem + - `supabase.ts` - Supabase client creation with environment validation + - `palette.ts` - Category color constants (CSS variables) and labels (en/de) + - `format.ts` - Currency formatting with Intl.NumberFormat API + - `utils.ts` - General helpers (like cn for class merging) + +**`src/i18n/`:** +- Purpose: Internationalization setup and resource files +- Contains: `index.ts` and JSON translation files +- Key files: + - `index.ts` - i18next initialization with react-i18next bindings + - `en.json` - English translation strings (namespaced by feature) + - `de.json` - German translation strings +- Initialized at app startup before any React render +- Provides `useTranslation()` hook for all components + +**`src/components/ui/`:** +- Purpose: Unstyled, accessible UI primitives from Shadcn and Radix UI +- Contains: 16 files of component exports +- Includes: Badge, Button, Card, Dialog, Dropdown Menu, Input, Label, Popover, Select, Separator, Sheet, Sidebar, Skeleton, Table, Tooltip, Sonner Toast wrapper +- Pattern: Each wraps Radix primitive with Tailwind styling +- Do NOT modify these files — regenerate via Shadcn CLI if needed + +## Key File Locations + +**Entry Points:** +- `src/main.tsx` - DOM mount point, providers initialization +- `src/App.tsx` - Route definitions and authentication guards +- `index.html` - Vite HTML template with root div + +**Configuration:** +- `vite.config.ts` - Build tooling (React plugin, Tailwind vite plugin, @ alias) +- `tsconfig.json` - Base TS config with @ path alias +- `eslint.config.js` - Linting rules +- `components.json` - Shadcn CLI registry +- `package.json` - Dependencies: react@19, react-router-dom@7, @tanstack/react-query@5, @supabase/supabase-js@2, i18next, lucide-react, recharts, tailwindcss@4, sonner + +**Core Logic:** +- `src/hooks/useBudgets.ts` - Largest hook (369 lines) with factory pattern for detail queries +- `src/hooks/useTemplate.ts` - Template mutations with sort_order management +- `src/lib/types.ts` - Single source of truth for domain types +- `src/lib/supabase.ts` - Client configuration (2 lines of config + validation) + +**Testing:** +- No test files present (no `*.test.ts`, `*.spec.ts`) +- No jest.config.js or vitest.config.ts + +## Naming Conventions + +**Files:** +- Pages: PascalCase with "Page" suffix (`DashboardPage.tsx`) +- Hooks: camelCase with "use" prefix (`useAuth.ts`, `useBudgets.ts`) +- Components: PascalCase (`AppLayout.tsx`, `QuickAddPicker.tsx`) +- Utilities: camelCase descriptive (`format.ts`, `palette.ts`) +- Types: camelCase file, PascalCase exports (`types.ts` exports `interface Budget`) +- UI components: kebab-case file, PascalCase export (`card.tsx` exports `Card`) + +**Directories:** +- Feature folders: lowercase plural (`src/hooks/`, `src/pages/`, `src/components/`) +- UI library: `ui/` subfolder under components + +**Functions & Variables:** +- Functions: camelCase (`formatCurrency`, `useBudgets`) +- Component functions: PascalCase (`DashboardPage`, `QuickAddPicker`) +- Constants: UPPER_SNAKE_CASE (`CATEGORY_TYPES`, `EXPENSE_TYPES`) +- Variables: camelCase (`budgetId`, `categoryId`, `isSaving`) + +**Types:** +- Interfaces: PascalCase (`Budget`, `BudgetItem`) +- Type unions: PascalCase (`CategoryType`) +- Props interfaces: PascalCase ending with "Props" (`QuickAddPickerProps`) + +## Where to Add New Code + +**New Feature (e.g., Reports):** +1. Create page: `src/pages/ReportsPage.tsx` +2. Create hook: `src/hooks/useReports.ts` with query keys and mutations +3. Add types: `src/lib/types.ts` - add new interfaces (Report, ReportItem) +4. Add route: `src/App.tsx` - add Route element +5. Add nav link: `src/components/AppLayout.tsx` - add to navItems array +6. Add i18n: `src/i18n/en.json` and `src/i18n/de.json` - add new keys + +**New Component (e.g., CategoryBadge):** +- If simple display component: `src/components/CategoryBadge.tsx` +- If UI primitive wrapper: `src/components/ui/category-badge.tsx` (follow shadcn pattern) +- Composition: Import from ui/ folder, layer styling via className + +**New Utility Function:** +- General helpers: `src/lib/utils.ts` +- Domain-specific (e.g., budget math): Add to relevant hook file or create `src/lib/budgetHelpers.ts` +- Formatting logic: `src/lib/format.ts` + +**New Hook:** +- Data fetching: `src/hooks/useFeatureName.ts` +- Pattern: Export named function, define query keys, use useQuery/useMutation, return typed object +- Example structure from `useTemplate.ts`: + - Query keys at top (const) + - Helper functions (async functions) + - Main hook function with useQuery/useMutation setup + - Exposed API object return + +**Styling:** +- Component styles: Tailwind className in JSX (no CSS files) +- Global styles: `src/index.css` (imports Tailwind directives) +- Color system: CSS variables in theme (Tailwind config) +- Category colors: `src/lib/palette.ts` (maps to CSS var(--color-X)) + +## Special Directories + +**`src/components/ui/`:** +- Purpose: Shadcn registry of unstyled, accessible Radix UI components +- Generated: Via `npx shadcn-ui@latest add [component]` +- Committed: Yes (production code) +- Do NOT hand-edit — regenerate if Shadcn updates + +**`public/`:** +- Purpose: Static assets (favicon, images, fonts) +- Generated: No +- Committed: Yes +- Served at root by Vite + +**`supabase/migrations/`:** +- Purpose: Database schema as versioned SQL files +- Generated: Via Supabase CLI +- Committed: Yes (tracked via git) +- Applied: By `supabase db push` command + +**`.env` files:** +- Purpose: Runtime configuration (Supabase URL, API key) +- Generated: Via `.env.example` template +- Committed: NO — in .gitignore +- Required for: Local dev and CI/CD + +**`dist/`:** +- Purpose: Production bundle output +- Generated: Via `npm run build` +- Committed: No — in .gitignore +- Deployment: Upload contents to CDN or web server + +## Code Organization Principles + +**Vertical Slices:** +- Feature → Page → Hook → Library +- Minimizes cross-feature coupling +- Easy to add/remove features + +**Co-location of Related Code:** +- Page component near its hooks +- Query keys defined in same hook as queries +- Mutations and queries in same hook for domain entity + +**Type Safety:** +- All Supabase queries cast return value to TypeScript type +- TanStack Query generic parameters: `useQuery()` and `useMutation()` +- Props interfaces for all custom components + +**Consistent Hook Patterns:** +- All data hooks follow: query setup → mutations setup → return typed object +- Mutations always have onSuccess cache invalidation +- Query keys are hierarchical arrays enabling granular invalidation + +--- + +*Structure analysis: 2026-03-16* diff --git a/.planning/codebase/TESTING.md b/.planning/codebase/TESTING.md new file mode 100644 index 0000000..d3b690c --- /dev/null +++ b/.planning/codebase/TESTING.md @@ -0,0 +1,397 @@ +# Testing Patterns + +**Analysis Date:** 2026-03-16 + +## Test Framework + +**Runner:** +- Not configured - No test framework installed +- No test files in `src/` directory +- No testing scripts in `package.json` +- No `vitest.config.ts`, `jest.config.ts`, or similar configuration + +**Assertion Library:** +- Not installed - No testing framework active + +**Run Commands:** +- No test commands available +- `npm run dev` - Development server +- `npm run build` - Production build +- `npm run lint` - ESLint only +- `npm run preview` - Preview built assets + +**Status:** Testing infrastructure not yet implemented. + +## Test File Organization + +**Location:** +- Not applicable - no test files exist +- Suggested pattern: Co-located tests +- Recommendation: Place `ComponentName.test.tsx` alongside `ComponentName.tsx` +- Recommendation: Place `hookName.test.ts` alongside `hookName.ts` + +**Naming:** +- `.test.ts` or `.test.tsx` suffix preferred for consistency with industry standard + +**Structure:** +- Suggested directory pattern: +``` +src/ +├── components/ +│ ├── QuickAddPicker.tsx +│ ├── QuickAddPicker.test.tsx +│ └── ui/ +│ ├── button.tsx +│ └── button.test.tsx +├── hooks/ +│ ├── useAuth.ts +│ ├── useAuth.test.ts +│ └── ... +└── lib/ + ├── utils.ts + ├── utils.test.ts + └── ... +``` + +## Test Structure + +**Suite Organization:** +- Not yet implemented +- Recommended framework: Vitest (lightweight, modern, TypeScript-first) +- Example pattern to implement: +```typescript +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' +import { useAuth } from '@/hooks/useAuth' + +describe('useAuth', () => { + beforeEach(() => { + // Setup + }) + + afterEach(() => { + // Cleanup + }) + + it('should load session on mount', () => { + // Test implementation + }) +}) +``` + +**Patterns to establish:** +- One top-level `describe` per hook/component +- Nested `describe` blocks for related test groups +- Each test file focuses on single module +- Use of `beforeEach` for setup, `afterEach` for cleanup + +## Mocking + +**Framework:** +- Not yet configured +- Recommended: Vitest with `vi` module for mocking +- Alternative: Mock Service Worker (MSW) for API mocking + +**Patterns to implement:** + +**Supabase Client Mocking:** +```typescript +vi.mock('@/lib/supabase', () => ({ + supabase: { + auth: { + getSession: vi.fn(), + getUser: vi.fn(), + onAuthStateChange: vi.fn(), + signUp: vi.fn(), + signIn: vi.fn(), + signOut: vi.fn(), + }, + from: vi.fn(), + }, +})) +``` + +**React Query Mocking:** +```typescript +vi.mock('@tanstack/react-query', () => ({ + useQuery: vi.fn(), + useMutation: vi.fn(), + useQueryClient: vi.fn(), +})) +``` + +**What to Mock:** +- External API calls (Supabase queries/mutations) +- React Query hooks (`useQuery`, `useMutation`) +- Toast notifications (`sonner`) +- Browser APIs (window, localStorage when needed) +- i18next translation function + +**What NOT to Mock:** +- Internal hook logic +- Component rendering +- State management patterns +- User interaction handlers (test actual behavior) + +## Fixtures and Factories + +**Test Data:** +- Not yet established +- Recommended location: `src/__tests__/fixtures/` or `src/__tests__/factories/` + +**Recommended pattern:** +```typescript +// src/__tests__/factories/category.factory.ts +import type { Category } from '@/lib/types' + +export function createCategory(overrides?: Partial): Category { + return { + id: crypto.randomUUID(), + user_id: 'test-user-id', + name: 'Test Category', + type: 'bill', + icon: null, + sort_order: 0, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + ...overrides, + } +} +``` + +**Location:** +- Suggested: `src/__tests__/factories/` for factory functions +- Suggested: `src/__tests__/fixtures/` for static test data +- Alternative: Inline factories in test files for simple cases + +## Coverage + +**Requirements:** +- Not enforced - No coverage configuration +- Recommended minimum: 70% for new code +- Critical paths: hooks and mutation handlers (highest priority) + +**View Coverage:** +- Once test framework installed, run: +```bash +npm run test:coverage +``` + +**Recommended coverage config (vitest.config.ts):** +```typescript +export default defineConfig({ + test: { + coverage: { + provider: 'v8', + reporter: ['text', 'html', 'json'], + exclude: [ + 'node_modules/', + 'src/__tests__/', + ], + }, + }, +}) +``` + +## Test Types + +**Unit Tests:** +- Scope: Individual functions, hooks, components in isolation +- Approach: Test pure logic without external dependencies +- Priority: Utility functions (`cn()`, `formatCurrency()`), custom hooks +- Example targets: + - `useBudgets()` query/mutation logic + - `useAuth()` session management + - `formatCurrency()` number formatting + - Validation logic in components + +**Integration Tests:** +- Scope: Multiple modules working together (e.g., hook + component) +- Approach: Mock Supabase, test hook + component interaction +- Priority: Complex components like `QuickAddPicker` with multiple state changes +- Example: Component flow - open popover → select item → open dialog → save + +**E2E Tests:** +- Framework: Not used +- Recommended: Playwright or Cypress for future implementation +- Focus areas: Full user workflows (login → create budget → add items) + +## Common Patterns + +**Async Testing:** +- Recommended approach with Vitest: +```typescript +it('should fetch categories', async () => { + const { result } = renderHook(() => useCategories()) + + await waitFor(() => { + expect(result.current.categories).toHaveLength(1) + }) +}) +``` + +- Alternative with MSW: +```typescript +it('should handle API error', async () => { + server.use( + http.get('/api/categories', () => { + return HttpResponse.error() + }) + ) + + const { result } = renderHook(() => useCategories()) + + await waitFor(() => { + expect(result.current.loading).toBe(false) + }) +}) +``` + +**Error Testing:** +- Test error states and error handling: +```typescript +it('should throw on auth error', async () => { + const mockSupabase = vi.mocked(supabase) + mockSupabase.auth.signIn.mockRejectedValueOnce( + new Error('Invalid credentials') + ) + + const { result } = renderHook(() => useAuth()) + + await expect(result.current.signIn('test@test.com', 'wrong')).rejects.toThrow() +}) +``` + +- Test error UI display: +```typescript +it('should display error message on login failure', async () => { + render() + + const input = screen.getByRole('textbox', { name: /email/i }) + await userEvent.type(input, 'test@test.com') + + await userEvent.click(screen.getByRole('button', { name: /sign in/i })) + + await waitFor(() => { + expect(screen.getByText(/invalid credentials/i)).toBeInTheDocument() + }) +}) +``` + +## React Testing Library Patterns + +**When to implement:** +- Recommended alongside unit tests for components +- Use `@testing-library/react` for component testing +- Use `@testing-library/user-event` for user interactions + +**Component test example structure:** +```typescript +import { render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import QuickAddPicker from '@/components/QuickAddPicker' + +describe('QuickAddPicker', () => { + const mockBudgetId = 'test-budget-id' + + it('should open popover on button click', async () => { + const user = userEvent.setup() + render() + + const button = screen.getByRole('button', { name: /quick add/i }) + await user.click(button) + + expect(screen.getByRole('listbox')).toBeInTheDocument() + }) +}) +``` + +## Setup and Configuration (Future) + +**Recommended `vitest.config.ts`:** +```typescript +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import path from 'path' + +export default defineConfig({ + plugins: [react()], + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['./src/__tests__/setup.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'html'], + include: ['src/**/*.{ts,tsx}'], + exclude: ['src/**/*.test.{ts,tsx}', 'src/**/*.d.ts'], + }, + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +}) +``` + +**Recommended `src/__tests__/setup.ts`:** +```typescript +import { beforeAll, afterEach, afterAll, vi } from 'vitest' +import { setupServer } from 'msw/node' +import { expect, afterEach as vitestAfterEach } from 'vitest' + +// Mock MSW server setup (if using MSW) +export const server = setupServer() + +beforeAll(() => { + server.listen({ onUnhandledRequest: 'error' }) +}) + +afterEach(() => { + server.resetHandlers() +}) + +afterAll(() => { + server.close() +}) +``` + +**Package additions needed:** +```json +{ + "devDependencies": { + "vitest": "^1.0.0", + "@testing-library/react": "^14.0.0", + "@testing-library/user-event": "^14.0.0", + "@testing-library/jest-dom": "^6.0.0", + "jsdom": "^23.0.0", + "msw": "^2.0.0" + } +} +``` + +## Critical Test Priorities + +**High Priority (Core Functionality):** +1. Hook mutations (`useBudgets.createBudget`, `useBudgets.generateFromTemplate`) +2. Authentication flow (`useAuth.signIn`, `useAuth.signOut`) +3. Complex component state (`QuickAddPicker` dialog flow) +4. Validation logic (form field checks) + +**Medium Priority (Data Access):** +1. Category queries and filtering +2. Budget item CRUD operations +3. Template copying logic +4. Sorting and ordering + +**Lower Priority (UI/Display):** +1. Component rendering +2. Conditional displays +3. Icon rendering +4. Theme switching + +--- + +*Testing analysis: 2026-03-16* + +**Note:** No test framework currently installed. This document provides guidance for future test implementation. Recommend prioritizing Vitest as lightweight TypeScript-native test framework complementing Vite build tooling already in use.