diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..783a84f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,64 @@ +# Claude Code Task Management Guide + +## Documentation Available + +📚 **Project Documentation**: Check the documentation files in this directory for project-specific setup instructions and guides. +**Project Tasks**: Check the tasks directory in documentation/tasks for the list of tasks to be completed. Use the CLI commands below to interact with them. + +## MANDATORY Task Management Workflow + +🚨 **YOU MUST FOLLOW THIS EXACT WORKFLOW - NO EXCEPTIONS** 🚨 + +### **STEP 1: DISCOVER TASKS (MANDATORY)** +You MUST start by running this command to see all available tasks: +```bash +task-manager list-tasks +``` + +### **STEP 2: START EACH TASK (MANDATORY)** +Before working on any task, you MUST mark it as started: +```bash +task-manager start-task +``` + +### **STEP 3: COMPLETE OR CANCEL EACH TASK (MANDATORY)** +After finishing implementation, you MUST mark the task as completed, or cancel if you cannot complete it: +```bash +task-manager complete-task "Brief description of what was implemented" +# or +task-manager cancel-task "Reason for cancellation" +``` + +## Task Files Location + +📁 **Task Data**: Your tasks are organized in the `documentation/tasks/` directory: +- Task JSON files contain complete task information +- Use ONLY the `task-manager` commands listed above +- Follow the mandatory workflow sequence for each task + +## MANDATORY Task Workflow Sequence + +🔄 **For EACH individual task, you MUST follow this sequence:** + +1. 📋 **DISCOVER**: `task-manager list-tasks` (first time only) +2. 🚀 **START**: `task-manager start-task ` (mark as in progress) +3. 💻 **IMPLEMENT**: Do the actual coding/implementation work +4. ✅ **COMPLETE**: `task-manager complete-task "What was done"` (or cancel with `task-manager cancel-task "Reason"`) +5. 🔁 **REPEAT**: Go to next task (start from step 2) + +## Task Status Options + +- `pending` - Ready to work on +- `in_progress` - Currently being worked on +- `completed` - Successfully finished +- `blocked` - Cannot proceed (waiting for dependencies) +- `cancelled` - No longer needed + +## CRITICAL WORKFLOW RULES + +❌ **NEVER skip** the `task-manager start-task` command +❌ **NEVER skip** the `task-manager complete-task` command (use `task-manager cancel-task` if a task is not planned, not required, or you must stop it) +❌ **NEVER work on multiple tasks simultaneously** +✅ **ALWAYS complete one task fully before starting the next** +✅ **ALWAYS provide completion details in the complete command** +✅ **ALWAYS follow the exact 3-step sequence: list → start → complete (or cancel if not required)** \ No newline at end of file diff --git a/app/api/finance/assets/[id]/route.ts b/app/api/finance/assets/[id]/route.ts new file mode 100644 index 0000000..c91b924 --- /dev/null +++ b/app/api/finance/assets/[id]/route.ts @@ -0,0 +1,113 @@ +import { NextRequest } from 'next/server'; +import { db, finance } from '@/db'; +import { getAuthenticatedUser, createErrorResponse, createSuccessResponse } from '@/lib/auth-utils'; +import { updateAssetSchema } from '@/lib/validations/finance'; +import { eq, and } from 'drizzle-orm'; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const user = await getAuthenticatedUser(); + if (!user) { + return createErrorResponse('Unauthorized', 401); + } + + const asset = await db + .select() + .from(finance.assets) + .where(and( + eq(finance.assets.id, id), + eq(finance.assets.userId, user.id) + )) + .limit(1); + + if (asset.length === 0) { + return createErrorResponse('Asset not found', 404); + } + + return createSuccessResponse(asset[0]); + } catch (error) { + console.error('Error fetching asset:', error); + return createErrorResponse('Internal server error', 500); + } +} + +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const user = await getAuthenticatedUser(); + if (!user) { + return createErrorResponse('Unauthorized', 401); + } + + const body = await request.json(); + const validationResult = updateAssetSchema.safeParse(body); + + if (!validationResult.success) { + return createErrorResponse( + validationResult.error.issues.map(e => e.message).join(', '), + 400 + ); + } + + const updateData = { ...validationResult.data, updatedAt: new Date() }; + + if (updateData.purchaseDate) { + updateData.purchaseDate = new Date(updateData.purchaseDate); + } + + const updatedAsset = await db + .update(finance.assets) + .set(updateData) + .where(and( + eq(finance.assets.id, id), + eq(finance.assets.userId, user.id) + )) + .returning(); + + if (updatedAsset.length === 0) { + return createErrorResponse('Asset not found', 404); + } + + return createSuccessResponse(updatedAsset[0]); + } catch (error) { + console.error('Error updating asset:', error); + return createErrorResponse('Internal server error', 500); + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const user = await getAuthenticatedUser(); + if (!user) { + return createErrorResponse('Unauthorized', 401); + } + + const deletedAsset = await db + .delete(finance.assets) + .where(and( + eq(finance.assets.id, id), + eq(finance.assets.userId, user.id) + )) + .returning(); + + if (deletedAsset.length === 0) { + return createErrorResponse('Asset not found', 404); + } + + return createSuccessResponse({ message: 'Asset deleted successfully' }); + } catch (error) { + console.error('Error deleting asset:', error); + return createErrorResponse('Internal server error', 500); + } +} \ No newline at end of file diff --git a/app/api/finance/assets/route.ts b/app/api/finance/assets/route.ts new file mode 100644 index 0000000..021101e --- /dev/null +++ b/app/api/finance/assets/route.ts @@ -0,0 +1,65 @@ +import { NextRequest } from 'next/server'; +import { db, finance } from '@/db'; +import { getAuthenticatedUser, createErrorResponse, createSuccessResponse } from '@/lib/auth-utils'; +import { createAssetSchema } from '@/lib/validations/finance'; +import { eq } from 'drizzle-orm'; + +export async function GET() { + try { + const user = await getAuthenticatedUser(); + if (!user) { + return createErrorResponse('Unauthorized', 401); + } + + const assets = await db + .select() + .from(finance.assets) + .where(eq(finance.assets.userId, user.id)); + + return createSuccessResponse(assets); + } catch (error) { + console.error('Error fetching assets:', error); + return createErrorResponse('Internal server error', 500); + } +} + +export async function POST(request: NextRequest) { + try { + const user = await getAuthenticatedUser(); + if (!user) { + return createErrorResponse('Unauthorized', 401); + } + + const body = await request.json(); + const validationResult = createAssetSchema.safeParse(body); + + if (!validationResult.success) { + return createErrorResponse( + validationResult.error.issues.map(e => e.message).join(', '), + 400 + ); + } + + const { name, type, value, description, purchaseDate } = validationResult.data; + + const newAsset = await db + .insert(finance.assets) + .values({ + id: crypto.randomUUID(), + userId: user.id, + name, + type, + value, + description, + purchaseDate: purchaseDate ? new Date(purchaseDate) : null, + createdAt: new Date(), + updatedAt: new Date(), + }) + .returning(); + + return createSuccessResponse(newAsset[0], 201); + } catch (error) { + console.error('Error creating asset:', error); + return createErrorResponse('Internal server error', 500); + } +} \ No newline at end of file diff --git a/app/api/finance/expenses/[id]/route.ts b/app/api/finance/expenses/[id]/route.ts new file mode 100644 index 0000000..9825809 --- /dev/null +++ b/app/api/finance/expenses/[id]/route.ts @@ -0,0 +1,113 @@ +import { NextRequest } from 'next/server'; +import { db, finance } from '@/db'; +import { getAuthenticatedUser, createErrorResponse, createSuccessResponse } from '@/lib/auth-utils'; +import { updateExpenseSchema } from '@/lib/validations/finance'; +import { eq, and } from 'drizzle-orm'; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const user = await getAuthenticatedUser(); + if (!user) { + return createErrorResponse('Unauthorized', 401); + } + + const expense = await db + .select() + .from(finance.expenses) + .where(and( + eq(finance.expenses.id, id), + eq(finance.expenses.userId, user.id) + )) + .limit(1); + + if (expense.length === 0) { + return createErrorResponse('Expense not found', 404); + } + + return createSuccessResponse(expense[0]); + } catch (error) { + console.error('Error fetching expense:', error); + return createErrorResponse('Internal server error', 500); + } +} + +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const user = await getAuthenticatedUser(); + if (!user) { + return createErrorResponse('Unauthorized', 401); + } + + const body = await request.json(); + const validationResult = updateExpenseSchema.safeParse(body); + + if (!validationResult.success) { + return createErrorResponse( + validationResult.error.issues.map(e => e.message).join(', '), + 400 + ); + } + + const updateData = { ...validationResult.data, updatedAt: new Date() }; + + if (updateData.date) { + updateData.date = new Date(updateData.date); + } + + const updatedExpense = await db + .update(finance.expenses) + .set(updateData) + .where(and( + eq(finance.expenses.id, id), + eq(finance.expenses.userId, user.id) + )) + .returning(); + + if (updatedExpense.length === 0) { + return createErrorResponse('Expense not found', 404); + } + + return createSuccessResponse(updatedExpense[0]); + } catch (error) { + console.error('Error updating expense:', error); + return createErrorResponse('Internal server error', 500); + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const user = await getAuthenticatedUser(); + if (!user) { + return createErrorResponse('Unauthorized', 401); + } + + const deletedExpense = await db + .delete(finance.expenses) + .where(and( + eq(finance.expenses.id, id), + eq(finance.expenses.userId, user.id) + )) + .returning(); + + if (deletedExpense.length === 0) { + return createErrorResponse('Expense not found', 404); + } + + return createSuccessResponse({ message: 'Expense deleted successfully' }); + } catch (error) { + console.error('Error deleting expense:', error); + return createErrorResponse('Internal server error', 500); + } +} \ No newline at end of file diff --git a/app/api/finance/expenses/route.ts b/app/api/finance/expenses/route.ts new file mode 100644 index 0000000..e586dd5 --- /dev/null +++ b/app/api/finance/expenses/route.ts @@ -0,0 +1,65 @@ +import { NextRequest } from 'next/server'; +import { db, finance } from '@/db'; +import { getAuthenticatedUser, createErrorResponse, createSuccessResponse } from '@/lib/auth-utils'; +import { createExpenseSchema } from '@/lib/validations/finance'; +import { eq } from 'drizzle-orm'; + +export async function GET() { + try { + const user = await getAuthenticatedUser(); + if (!user) { + return createErrorResponse('Unauthorized', 401); + } + + const expenses = await db + .select() + .from(finance.expenses) + .where(eq(finance.expenses.userId, user.id)); + + return createSuccessResponse(expenses); + } catch (error) { + console.error('Error fetching expenses:', error); + return createErrorResponse('Internal server error', 500); + } +} + +export async function POST(request: NextRequest) { + try { + const user = await getAuthenticatedUser(); + if (!user) { + return createErrorResponse('Unauthorized', 401); + } + + const body = await request.json(); + const validationResult = createExpenseSchema.safeParse(body); + + if (!validationResult.success) { + return createErrorResponse( + validationResult.error.issues.map(e => e.message).join(', '), + 400 + ); + } + + const { title, amount, category, description, date, isRecurring } = validationResult.data; + + const newExpense = await db + .insert(finance.expenses) + .values({ + userId: user.id, + title, + amount, + category, + description, + date: new Date(date), + isRecurring: isRecurring || null, + createdAt: new Date(), + updatedAt: new Date(), + }) + .returning(); + + return createSuccessResponse(newExpense[0], 201); + } catch (error) { + console.error('Error creating expense:', error); + return createErrorResponse('Internal server error', 500); + } +} \ No newline at end of file diff --git a/app/api/finance/income/[id]/route.ts b/app/api/finance/income/[id]/route.ts new file mode 100644 index 0000000..aa86b2b --- /dev/null +++ b/app/api/finance/income/[id]/route.ts @@ -0,0 +1,113 @@ +import { NextRequest } from 'next/server'; +import { db, finance } from '@/db'; +import { getAuthenticatedUser, createErrorResponse, createSuccessResponse } from '@/lib/auth-utils'; +import { updateIncomeSchema } from '@/lib/validations/finance'; +import { eq, and } from 'drizzle-orm'; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const user = await getAuthenticatedUser(); + if (!user) { + return createErrorResponse('Unauthorized', 401); + } + + const income = await db + .select() + .from(finance.income) + .where(and( + eq(finance.income.id, id), + eq(finance.income.userId, user.id) + )) + .limit(1); + + if (income.length === 0) { + return createErrorResponse('Income not found', 404); + } + + return createSuccessResponse(income[0]); + } catch (error) { + console.error('Error fetching income:', error); + return createErrorResponse('Internal server error', 500); + } +} + +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const user = await getAuthenticatedUser(); + if (!user) { + return createErrorResponse('Unauthorized', 401); + } + + const body = await request.json(); + const validationResult = updateIncomeSchema.safeParse(body); + + if (!validationResult.success) { + return createErrorResponse( + validationResult.error.issues.map(e => e.message).join(', '), + 400 + ); + } + + const updateData = { ...validationResult.data, updatedAt: new Date() }; + + if (updateData.date) { + updateData.date = new Date(updateData.date); + } + + const updatedIncome = await db + .update(finance.income) + .set(updateData) + .where(and( + eq(finance.income.id, id), + eq(finance.income.userId, user.id) + )) + .returning(); + + if (updatedIncome.length === 0) { + return createErrorResponse('Income not found', 404); + } + + return createSuccessResponse(updatedIncome[0]); + } catch (error) { + console.error('Error updating income:', error); + return createErrorResponse('Internal server error', 500); + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const user = await getAuthenticatedUser(); + if (!user) { + return createErrorResponse('Unauthorized', 401); + } + + const deletedIncome = await db + .delete(finance.income) + .where(and( + eq(finance.income.id, id), + eq(finance.income.userId, user.id) + )) + .returning(); + + if (deletedIncome.length === 0) { + return createErrorResponse('Income not found', 404); + } + + return createSuccessResponse({ message: 'Income deleted successfully' }); + } catch (error) { + console.error('Error deleting income:', error); + return createErrorResponse('Internal server error', 500); + } +} \ No newline at end of file diff --git a/app/api/finance/income/route.ts b/app/api/finance/income/route.ts new file mode 100644 index 0000000..c6452c2 --- /dev/null +++ b/app/api/finance/income/route.ts @@ -0,0 +1,65 @@ +import { NextRequest } from 'next/server'; +import { db, finance } from '@/db'; +import { getAuthenticatedUser, createErrorResponse, createSuccessResponse } from '@/lib/auth-utils'; +import { createIncomeSchema } from '@/lib/validations/finance'; +import { eq } from 'drizzle-orm'; + +export async function GET() { + try { + const user = await getAuthenticatedUser(); + if (!user) { + return createErrorResponse('Unauthorized', 401); + } + + const income = await db + .select() + .from(finance.income) + .where(eq(finance.income.userId, user.id)); + + return createSuccessResponse(income); + } catch (error) { + console.error('Error fetching income:', error); + return createErrorResponse('Internal server error', 500); + } +} + +export async function POST(request: NextRequest) { + try { + const user = await getAuthenticatedUser(); + if (!user) { + return createErrorResponse('Unauthorized', 401); + } + + const body = await request.json(); + const validationResult = createIncomeSchema.safeParse(body); + + if (!validationResult.success) { + return createErrorResponse( + validationResult.error.issues.map(e => e.message).join(', '), + 400 + ); + } + + const { title, amount, type, description, date, isRecurring } = validationResult.data; + + const newIncome = await db + .insert(finance.income) + .values({ + userId: user.id, + title, + amount, + type, + description, + date: new Date(date), + isRecurring: isRecurring || null, + createdAt: new Date(), + updatedAt: new Date(), + }) + .returning(); + + return createSuccessResponse(newIncome[0], 201); + } catch (error) { + console.error('Error creating income:', error); + return createErrorResponse('Internal server error', 500); + } +} \ No newline at end of file diff --git a/app/api/finance/investments/[id]/route.ts b/app/api/finance/investments/[id]/route.ts new file mode 100644 index 0000000..430b5de --- /dev/null +++ b/app/api/finance/investments/[id]/route.ts @@ -0,0 +1,113 @@ +import { NextRequest } from 'next/server'; +import { db, finance } from '@/db'; +import { getAuthenticatedUser, createErrorResponse, createSuccessResponse } from '@/lib/auth-utils'; +import { updateInvestmentSchema } from '@/lib/validations/finance'; +import { eq, and } from 'drizzle-orm'; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const user = await getAuthenticatedUser(); + if (!user) { + return createErrorResponse('Unauthorized', 401); + } + + const investment = await db + .select() + .from(finance.investments) + .where(and( + eq(finance.investments.id, id), + eq(finance.investments.userId, user.id) + )) + .limit(1); + + if (investment.length === 0) { + return createErrorResponse('Investment not found', 404); + } + + return createSuccessResponse(investment[0]); + } catch (error) { + console.error('Error fetching investment:', error); + return createErrorResponse('Internal server error', 500); + } +} + +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const user = await getAuthenticatedUser(); + if (!user) { + return createErrorResponse('Unauthorized', 401); + } + + const body = await request.json(); + const validationResult = updateInvestmentSchema.safeParse(body); + + if (!validationResult.success) { + return createErrorResponse( + validationResult.error.issues.map(e => e.message).join(', '), + 400 + ); + } + + const updateData = { ...validationResult.data, updatedAt: new Date() }; + + if (updateData.purchaseDate) { + updateData.purchaseDate = new Date(updateData.purchaseDate); + } + + const updatedInvestment = await db + .update(finance.investments) + .set(updateData) + .where(and( + eq(finance.investments.id, id), + eq(finance.investments.userId, user.id) + )) + .returning(); + + if (updatedInvestment.length === 0) { + return createErrorResponse('Investment not found', 404); + } + + return createSuccessResponse(updatedInvestment[0]); + } catch (error) { + console.error('Error updating investment:', error); + return createErrorResponse('Internal server error', 500); + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const user = await getAuthenticatedUser(); + if (!user) { + return createErrorResponse('Unauthorized', 401); + } + + const deletedInvestment = await db + .delete(finance.investments) + .where(and( + eq(finance.investments.id, id), + eq(finance.investments.userId, user.id) + )) + .returning(); + + if (deletedInvestment.length === 0) { + return createErrorResponse('Investment not found', 404); + } + + return createSuccessResponse({ message: 'Investment deleted successfully' }); + } catch (error) { + console.error('Error deleting investment:', error); + return createErrorResponse('Internal server error', 500); + } +} \ No newline at end of file diff --git a/app/api/finance/investments/route.ts b/app/api/finance/investments/route.ts new file mode 100644 index 0000000..1409c4c --- /dev/null +++ b/app/api/finance/investments/route.ts @@ -0,0 +1,67 @@ +import { NextRequest } from 'next/server'; +import { db, finance } from '@/db'; +import { getAuthenticatedUser, createErrorResponse, createSuccessResponse } from '@/lib/auth-utils'; +import { createInvestmentSchema } from '@/lib/validations/finance'; +import { eq } from 'drizzle-orm'; + +export async function GET() { + try { + const user = await getAuthenticatedUser(); + if (!user) { + return createErrorResponse('Unauthorized', 401); + } + + const investments = await db + .select() + .from(finance.investments) + .where(eq(finance.investments.userId, user.id)); + + return createSuccessResponse(investments); + } catch (error) { + console.error('Error fetching investments:', error); + return createErrorResponse('Internal server error', 500); + } +} + +export async function POST(request: NextRequest) { + try { + const user = await getAuthenticatedUser(); + if (!user) { + return createErrorResponse('Unauthorized', 401); + } + + const body = await request.json(); + const validationResult = createInvestmentSchema.safeParse(body); + + if (!validationResult.success) { + return createErrorResponse( + validationResult.error.issues.map(e => e.message).join(', '), + 400 + ); + } + + const { name, type, symbol, quantity, purchasePrice, currentPrice, description, purchaseDate } = validationResult.data; + + const newInvestment = await db + .insert(finance.investments) + .values({ + userId: user.id, + name, + type, + symbol: symbol || null, + quantity, + purchasePrice, + currentPrice: currentPrice || null, + description, + purchaseDate: new Date(purchaseDate), + createdAt: new Date(), + updatedAt: new Date(), + }) + .returning(); + + return createSuccessResponse(newInvestment[0], 201); + } catch (error) { + console.error('Error creating investment:', error); + return createErrorResponse('Internal server error', 500); + } +} \ No newline at end of file diff --git a/app/dashboard/assets/page.tsx b/app/dashboard/assets/page.tsx new file mode 100644 index 0000000..62c3a28 --- /dev/null +++ b/app/dashboard/assets/page.tsx @@ -0,0 +1,481 @@ +"use client"; + +import { useState } from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Plus, Edit, Trash2, Wallet, Home, Car, Banknote, AlertCircle, RefreshCw } from "lucide-react"; +import { ASSET_TYPES, Asset } from "@/lib/types/finance"; +import { useAssets } from "@/hooks/use-assets"; + + +const getAssetIcon = (type: string) => { + switch (type) { + case 'real_estate': + return ; + case 'vehicle': + return ; + case 'savings': + case 'cash': + return ; + default: + return ; + } +}; + +const getAssetTypeLabel = (type: string) => { + return ASSET_TYPES.find(t => t.value === type)?.label || type; +}; + +export default function AssetsPage() { + const { assets, isLoading, error, createAsset, updateAsset, deleteAsset, refetch } = useAssets(); + const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); + const [editingAsset, setEditingAsset] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + const [formData, setFormData] = useState({ + name: "", + type: "", + value: "", + description: "", + purchaseDate: "", + }); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsSubmitting(true); + + const data = { + name: formData.name, + type: formData.type, + value: parseFloat(formData.value), + description: formData.description || undefined, + purchaseDate: formData.purchaseDate || undefined, + }; + + try { + let success = false; + if (editingAsset) { + success = await updateAsset(editingAsset.id, { + name: data.name, + type: data.type as "real_estate" | "vehicle" | "cash" | "savings" | "other", + value: String(data.value) , + description: data.description, + purchaseDate: data.purchaseDate, + }); + if (success) { + setEditingAsset(null); + } + } else { + success = await createAsset({ + name: data.name, + type: data.type as "real_estate" | "vehicle" | "cash" | "savings" | "other", + value: String(data.value), + description: data.description, + purchaseDate: data.purchaseDate, + }); + if (success) { + setIsAddDialogOpen(false); + } + } + + if (success) { + setFormData({ name: "", type: "", value: "", description: "", purchaseDate: "" }); + } + } catch (err) { + // Error is handled by the hook + } finally { + setIsSubmitting(false); + } + }; + + const handleEdit = (asset: Asset) => { + setEditingAsset(asset); + setFormData({ + name: asset.name, + type: asset.type, + value: asset.value.toString(), + description: asset.description || "", + purchaseDate: asset.purchaseDate ? new Date(asset.purchaseDate).toISOString().split('T')[0] : "", + }); + }; + + const handleDelete = async (id: string) => { + if (window.confirm('Are you sure you want to delete this asset?')) { + await deleteAsset(id); + } + }; + + const totalValue = assets.reduce((sum, asset) => sum + Number(asset.value), 0); + + if (error) { + return ( +
+
+

Assets

+

+ Manage your assets and track their value over time +

+
+ + + + Error loading assets: {error} + + + +
+ ); + } + + return ( +
+ {/* Header */} +
+
+

Assets

+

+ Manage your assets and track their value over time +

+
+ + + + + + + Add New Asset + + Enter the details of your new asset. + + +
+
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="Asset name" + required + /> +
+ +
+ + +
+ +
+ + setFormData({ ...formData, value: e.target.value })} + placeholder="0.00" + required + /> +
+ +
+ + setFormData({ ...formData, purchaseDate: e.target.value })} + /> +
+ +
+ +