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/route.ts b/app/api/finance/assets/route.ts new file mode 100644 index 0000000..2ab7c63 --- /dev/null +++ b/app/api/finance/assets/route.ts @@ -0,0 +1,136 @@ +import { NextRequest } from "next/server"; +import { db } from "@/db"; +import { assets } from "@/db/schema/finance"; +import { eq, and } from "drizzle-orm"; +import { assetCreateSchema, assetUpdateSchema } from "@/lib/validation"; +import { createApiResponse, handleApiError, handleValidationError } from "@/lib/api-utils"; +import { getSession } from "@/lib/auth-client"; +import { z } from "zod"; + +// GET /api/finance/assets - Get all assets for current user +export async function GET(request: NextRequest) { + try { + const session = await getSession(); + + if (!session?.user) { + return createApiResponse(false, undefined, "Unauthorized", 401); + } + + const userAssets = await db + .select() + .from(assets) + .where(eq(assets.userId, session.user.id)) + .orderBy(assets.createdAt); + + return createApiResponse(true, userAssets); + } catch (error) { + return handleApiError(error); + } +} + +// POST /api/finance/assets - Create a new asset +export async function POST(request: NextRequest) { + try { + const session = await getSession(); + + if (!session?.user) { + return createApiResponse(false, undefined, "Unauthorized", 401); + } + + const body = await request.json(); + const validatedData = assetCreateSchema.parse(body); + + const [newAsset] = await db + .insert(assets) + .values({ + id: crypto.randomUUID(), + userId: session.user.id, + name: validatedData.name, + type: validatedData.type, + value: validatedData.value.toString(), + purchaseDate: validatedData.purchaseDate ? new Date(validatedData.purchaseDate) : undefined, + description: validatedData.description, + }) + .returning(); + + return createApiResponse(true, newAsset, undefined, 201); + } catch (error) { + if (error instanceof z.ZodError) { + return handleValidationError(error); + } + return handleApiError(error); + } +} + +// PUT /api/finance/assets - Update an asset +export async function PUT(request: NextRequest) { + try { + const session = await getSession(); + + if (!session?.user) { + return createApiResponse(false, undefined, "Unauthorized", 401); + } + + const body = await request.json(); + const { id, ...updateData } = body; + + if (!id) { + return createApiResponse(false, undefined, "Asset ID is required", 400); + } + + const validatedData = assetUpdateSchema.parse(updateData); + + const [updatedAsset] = await db + .update(assets) + .set({ + ...validatedData, + value: validatedData.value ? validatedData.value.toString() : undefined, + purchaseDate: validatedData.purchaseDate ? new Date(validatedData.purchaseDate) : undefined, + updatedAt: new Date(), + }) + .where(and(eq(assets.id, id), eq(assets.userId, session.user.id))) + .returning(); + + if (!updatedAsset) { + return createApiResponse(false, undefined, "Asset not found", 404); + } + + return createApiResponse(true, updatedAsset); + } catch (error) { + if (error instanceof z.ZodError) { + return handleValidationError(error); + } + return handleApiError(error); + } +} + +// DELETE /api/finance/assets - Delete an asset +export async function DELETE(request: NextRequest) { + try { + const session = await getSession(); + + if (!session?.user) { + return createApiResponse(false, undefined, "Unauthorized", 401); + } + + const { searchParams } = new URL(request.url); + const id = searchParams.get("id"); + + if (!id) { + return createApiResponse(false, undefined, "Asset ID is required", 400); + } + + const [deletedAsset] = await db + .delete(assets) + .where(and(eq(assets.id, id), eq(assets.userId, session.user.id))) + .returning(); + + if (!deletedAsset) { + return createApiResponse(false, undefined, "Asset not found", 404); + } + + return createApiResponse(true, { message: "Asset deleted successfully" }); + } catch (error) { + return handleApiError(error); + } +} \ 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..ef0bde0 --- /dev/null +++ b/app/api/finance/expenses/route.ts @@ -0,0 +1,138 @@ +import { NextRequest } from "next/server"; +import { db } from "@/db"; +import { expenses } from "@/db/schema/finance"; +import { eq, and } from "drizzle-orm"; +import { expenseCreateSchema, expenseUpdateSchema } from "@/lib/validation"; +import { createApiResponse, handleApiError, handleValidationError } from "@/lib/api-utils"; +import { getSession } from "@/lib/auth-client"; +import { z } from "zod"; + +// GET /api/finance/expenses - Get all expenses for current user +export async function GET(request: NextRequest) { + try { + const session = await getSession(); + + if (!session?.user) { + return createApiResponse(false, undefined, "Unauthorized", 401); + } + + const userExpenses = await db + .select() + .from(expenses) + .where(eq(expenses.userId, session.user.id)) + .orderBy(expenses.date); + + return createApiResponse(true, userExpenses); + } catch (error) { + return handleApiError(error); + } +} + +// POST /api/finance/expenses - Create a new expense +export async function POST(request: NextRequest) { + try { + const session = await getSession(); + + if (!session?.user) { + return createApiResponse(false, undefined, "Unauthorized", 401); + } + + const body = await request.json(); + const validatedData = expenseCreateSchema.parse(body); + + const [newExpense] = await db + .insert(expenses) + .values({ + id: crypto.randomUUID(), + userId: session.user.id, + description: validatedData.description, + amount: validatedData.amount.toString(), + category: validatedData.category, + date: new Date(validatedData.date), + isRecurring: validatedData.isRecurring, + recurringFrequency: validatedData.recurringFrequency, + notes: validatedData.notes, + }) + .returning(); + + return createApiResponse(true, newExpense, undefined, 201); + } catch (error) { + if (error instanceof z.ZodError) { + return handleValidationError(error); + } + return handleApiError(error); + } +} + +// PUT /api/finance/expenses - Update an expense +export async function PUT(request: NextRequest) { + try { + const session = await getSession(); + + if (!session?.user) { + return createApiResponse(false, undefined, "Unauthorized", 401); + } + + const body = await request.json(); + const { id, ...updateData } = body; + + if (!id) { + return createApiResponse(false, undefined, "Expense ID is required", 400); + } + + const validatedData = expenseUpdateSchema.parse(updateData); + + const [updatedExpense] = await db + .update(expenses) + .set({ + ...validatedData, + amount: validatedData.amount ? validatedData.amount.toString() : undefined, + date: validatedData.date ? new Date(validatedData.date) : undefined, + updatedAt: new Date(), + }) + .where(and(eq(expenses.id, id), eq(expenses.userId, session.user.id))) + .returning(); + + if (!updatedExpense) { + return createApiResponse(false, undefined, "Expense not found", 404); + } + + return createApiResponse(true, updatedExpense); + } catch (error) { + if (error instanceof z.ZodError) { + return handleValidationError(error); + } + return handleApiError(error); + } +} + +// DELETE /api/finance/expenses - Delete an expense +export async function DELETE(request: NextRequest) { + try { + const session = await getSession(); + + if (!session?.user) { + return createApiResponse(false, undefined, "Unauthorized", 401); + } + + const { searchParams } = new URL(request.url); + const id = searchParams.get("id"); + + if (!id) { + return createApiResponse(false, undefined, "Expense ID is required", 400); + } + + const [deletedExpense] = await db + .delete(expenses) + .where(and(eq(expenses.id, id), eq(expenses.userId, session.user.id))) + .returning(); + + if (!deletedExpense) { + return createApiResponse(false, undefined, "Expense not found", 404); + } + + return createApiResponse(true, { message: "Expense deleted successfully" }); + } catch (error) { + return handleApiError(error); + } +} \ 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..78c22da --- /dev/null +++ b/app/api/finance/income/route.ts @@ -0,0 +1,137 @@ +import { NextRequest } from "next/server"; +import { db } from "@/db"; +import { income } from "@/db/schema/finance"; +import { eq, and } from "drizzle-orm"; +import { incomeCreateSchema, incomeUpdateSchema } from "@/lib/validation"; +import { createApiResponse, handleApiError, handleValidationError } from "@/lib/api-utils"; +import { getSession } from "@/lib/auth-client"; +import { z } from "zod"; + +// GET /api/finance/income - Get all income records for current user +export async function GET(request: NextRequest) { + try { + const session = await getSession(); + + if (!session?.user) { + return createApiResponse(false, undefined, "Unauthorized", 401); + } + + const userIncome = await db + .select() + .from(income) + .where(eq(income.userId, session.user.id)) + .orderBy(income.date); + + return createApiResponse(true, userIncome); + } catch (error) { + return handleApiError(error); + } +} + +// POST /api/finance/income - Create a new income record +export async function POST(request: NextRequest) { + try { + const session = await getSession(); + + if (!session?.user) { + return createApiResponse(false, undefined, "Unauthorized", 401); + } + + const body = await request.json(); + const validatedData = incomeCreateSchema.parse(body); + + const [newIncome] = await db + .insert(income) + .values({ + id: crypto.randomUUID(), + userId: session.user.id, + source: validatedData.source, + amount: validatedData.amount.toString(), + frequency: validatedData.frequency, + date: new Date(validatedData.date), + isRecurring: validatedData.isRecurring, + description: validatedData.description, + }) + .returning(); + + return createApiResponse(true, newIncome, undefined, 201); + } catch (error) { + if (error instanceof z.ZodError) { + return handleValidationError(error); + } + return handleApiError(error); + } +} + +// PUT /api/finance/income - Update an income record +export async function PUT(request: NextRequest) { + try { + const session = await getSession(); + + if (!session?.user) { + return createApiResponse(false, undefined, "Unauthorized", 401); + } + + const body = await request.json(); + const { id, ...updateData } = body; + + if (!id) { + return createApiResponse(false, undefined, "Income ID is required", 400); + } + + const validatedData = incomeUpdateSchema.parse(updateData); + + const [updatedIncome] = await db + .update(income) + .set({ + ...validatedData, + amount: validatedData.amount ? validatedData.amount.toString() : undefined, + date: validatedData.date ? new Date(validatedData.date) : undefined, + updatedAt: new Date(), + }) + .where(and(eq(income.id, id), eq(income.userId, session.user.id))) + .returning(); + + if (!updatedIncome) { + return createApiResponse(false, undefined, "Income record not found", 404); + } + + return createApiResponse(true, updatedIncome); + } catch (error) { + if (error instanceof z.ZodError) { + return handleValidationError(error); + } + return handleApiError(error); + } +} + +// DELETE /api/finance/income - Delete an income record +export async function DELETE(request: NextRequest) { + try { + const session = await getSession(); + + if (!session?.user) { + return createApiResponse(false, undefined, "Unauthorized", 401); + } + + const { searchParams } = new URL(request.url); + const id = searchParams.get("id"); + + if (!id) { + return createApiResponse(false, undefined, "Income ID is required", 400); + } + + const [deletedIncome] = await db + .delete(income) + .where(and(eq(income.id, id), eq(income.userId, session.user.id))) + .returning(); + + if (!deletedIncome) { + return createApiResponse(false, undefined, "Income record not found", 404); + } + + return createApiResponse(true, { message: "Income record deleted successfully" }); + } catch (error) { + return handleApiError(error); + } +} \ 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..958bd94 --- /dev/null +++ b/app/api/finance/investments/route.ts @@ -0,0 +1,140 @@ +import { NextRequest } from "next/server"; +import { db } from "@/db"; +import { investments } from "@/db/schema/finance"; +import { eq, and } from "drizzle-orm"; +import { investmentCreateSchema, investmentUpdateSchema } from "@/lib/validation"; +import { createApiResponse, handleApiError, handleValidationError } from "@/lib/api-utils"; +import { getSession } from "@/lib/auth-client"; +import { z } from "zod"; + +// GET /api/finance/investments - Get all investments for current user +export async function GET(request: NextRequest) { + try { + const session = await getSession(); + + if (!session?.user) { + return createApiResponse(false, undefined, "Unauthorized", 401); + } + + const userInvestments = await db + .select() + .from(investments) + .where(eq(investments.userId, session.user.id)) + .orderBy(investments.createdAt); + + return createApiResponse(true, userInvestments); + } catch (error) { + return handleApiError(error); + } +} + +// POST /api/finance/investments - Create a new investment +export async function POST(request: NextRequest) { + try { + const session = await getSession(); + + if (!session?.user) { + return createApiResponse(false, undefined, "Unauthorized", 401); + } + + const body = await request.json(); + const validatedData = investmentCreateSchema.parse(body); + + const [newInvestment] = await db + .insert(investments) + .values({ + id: crypto.randomUUID(), + userId: session.user.id, + name: validatedData.name, + type: validatedData.type, + amount: validatedData.amount.toString(), + currentValue: validatedData.currentValue.toString(), + purchaseDate: new Date(validatedData.purchaseDate), + tickerSymbol: validatedData.tickerSymbol, + description: validatedData.description, + isActive: validatedData.isActive, + }) + .returning(); + + return createApiResponse(true, newInvestment, undefined, 201); + } catch (error) { + if (error instanceof z.ZodError) { + return handleValidationError(error); + } + return handleApiError(error); + } +} + +// PUT /api/finance/investments - Update an investment +export async function PUT(request: NextRequest) { + try { + const session = await getSession(); + + if (!session?.user) { + return createApiResponse(false, undefined, "Unauthorized", 401); + } + + const body = await request.json(); + const { id, ...updateData } = body; + + if (!id) { + return createApiResponse(false, undefined, "Investment ID is required", 400); + } + + const validatedData = investmentUpdateSchema.parse(updateData); + + const [updatedInvestment] = await db + .update(investments) + .set({ + ...validatedData, + amount: validatedData.amount ? validatedData.amount.toString() : undefined, + currentValue: validatedData.currentValue ? validatedData.currentValue.toString() : undefined, + purchaseDate: validatedData.purchaseDate ? new Date(validatedData.purchaseDate) : undefined, + updatedAt: new Date(), + }) + .where(and(eq(investments.id, id), eq(investments.userId, session.user.id))) + .returning(); + + if (!updatedInvestment) { + return createApiResponse(false, undefined, "Investment not found", 404); + } + + return createApiResponse(true, updatedInvestment); + } catch (error) { + if (error instanceof z.ZodError) { + return handleValidationError(error); + } + return handleApiError(error); + } +} + +// DELETE /api/finance/investments - Delete an investment +export async function DELETE(request: NextRequest) { + try { + const session = await getSession(); + + if (!session?.user) { + return createApiResponse(false, undefined, "Unauthorized", 401); + } + + const { searchParams } = new URL(request.url); + const id = searchParams.get("id"); + + if (!id) { + return createApiResponse(false, undefined, "Investment ID is required", 400); + } + + const [deletedInvestment] = await db + .delete(investments) + .where(and(eq(investments.id, id), eq(investments.userId, session.user.id))) + .returning(); + + if (!deletedInvestment) { + return createApiResponse(false, undefined, "Investment not found", 404); + } + + return createApiResponse(true, { message: "Investment deleted successfully" }); + } catch (error) { + return handleApiError(error); + } +} \ No newline at end of file diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index d1765ef..224aa7f 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,18 +1,587 @@ -import { ChartAreaInteractive } from "@//components/chart-area-interactive" -import { DataTable } from "@//components/data-table" -import { SectionCards } from "@//components/section-cards" -import data from "@/app/dashboard/data.json" +"use client"; + +import { useState, useEffect } from "react"; +import { FinanceSummaryCards } from "@/components/finance-summary-cards"; +import { FinanceDataTable } from "@/components/finance-data-table"; +import { FinanceFormModal } from "@/components/finance-form-modal"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Plus, Landmark, TrendingUp, Wallet, DollarSign } from "lucide-react"; +import { ColumnDef } from "@tanstack/react-table"; +import { Badge } from "@/components/ui/badge"; +import { toast } from "sonner"; + +interface Asset { + id: string; + name: string; + type: string; + value: string; + purchaseDate?: string; + description?: string; + createdAt: string; +} + +interface Investment { + id: string; + name: string; + type: string; + amount: string; + currentValue: string; + purchaseDate: string; + tickerSymbol?: string; + isActive: boolean; + createdAt: string; +} + +interface Expense { + id: string; + description: string; + amount: string; + category: string; + date: string; + isRecurring: boolean; + createdAt: string; +} + +interface Income { + id: string; + source: string; + amount: string; + frequency: string; + date: string; + isRecurring: boolean; + createdAt: string; +} export default function Page() { + const [assets, setAssets] = useState([]); + const [investments, setInvestments] = useState([]); + const [expenses, setExpenses] = useState([]); + const [income, setIncome] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetchAllData(); + }, []); + + const fetchAllData = async () => { + try { + setLoading(true); + + // Fetch data from API endpoints + const [assetsRes, investmentsRes, expensesRes, incomeRes] = await Promise.all([ + fetch('/api/finance/assets'), + fetch('/api/finance/investments'), + fetch('/api/finance/expenses'), + fetch('/api/finance/income') + ]); + + if (assetsRes.ok) { + const assetsData = await assetsRes.json(); + setAssets(assetsData.data || []); + } + + if (investmentsRes.ok) { + const investmentsData = await investmentsRes.json(); + setInvestments(investmentsData.data || []); + } + + if (expensesRes.ok) { + const expensesData = await expensesRes.json(); + setExpenses(expensesData.data || []); + } + + if (incomeRes.ok) { + const incomeData = await incomeRes.json(); + setIncome(incomeData.data || []); + } + + } catch { + toast.error("Failed to load financial data"); + } finally { + setLoading(false); + } + }; + + const handleCreate = async (type: string, data: Record) => { + try { + const response = await fetch(`/api/finance/${type}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + + if (response.ok) { + const result = await response.json(); + + // Update local state with the new item + switch (type) { + case "asset": + setAssets(prev => [...prev, result.data]); + break; + case "investment": + setInvestments(prev => [...prev, result.data]); + break; + case "expense": + setExpenses(prev => [...prev, result.data]); + break; + case "income": + setIncome(prev => [...prev, result.data]); + break; + } + + toast.success(`${type.charAt(0).toUpperCase() + type.slice(1)} created successfully`); + } else { + const error = await response.json(); + toast.error(error.error || `Failed to create ${type}`); + } + } catch { + toast.error(`Failed to create ${type}`); + } + }; + + const handleDelete = async (type: string, id: string) => { + try { + const response = await fetch(`/api/finance/${type}/${id}`, { + method: 'DELETE', + }); + + if (response.ok) { + // Update local state by removing the deleted item + switch (type) { + case "asset": + setAssets(prev => prev.filter(item => item.id !== id)); + break; + case "investment": + setInvestments(prev => prev.filter(item => item.id !== id)); + break; + case "expense": + setExpenses(prev => prev.filter(item => item.id !== id)); + break; + case "income": + setIncome(prev => prev.filter(item => item.id !== id)); + break; + } + + toast.success(`${type.charAt(0).toUpperCase() + type.slice(1)} deleted successfully`); + } else { + const error = await response.json(); + toast.error(error.error || `Failed to delete ${type}`); + } + } catch { + toast.error(`Failed to delete ${type}`); + } + }; + + // Column definitions for data tables + const assetColumns: ColumnDef[] = [ + { accessorKey: "name", header: "Name" }, + { accessorKey: "type", header: "Type" }, + { + accessorKey: "value", + header: "Value", + cell: ({ row }) => `$${parseFloat(row.getValue("value")).toLocaleString()}` + }, + { accessorKey: "purchaseDate", header: "Purchase Date" }, + { accessorKey: "description", header: "Description" } + ]; + + const investmentColumns: ColumnDef[] = [ + { accessorKey: "name", header: "Name" }, + { accessorKey: "type", header: "Type" }, + { accessorKey: "tickerSymbol", header: "Symbol" }, + { + accessorKey: "amount", + header: "Invested", + cell: ({ row }) => `$${parseFloat(row.getValue("amount")).toLocaleString()}` + }, + { + accessorKey: "currentValue", + header: "Current Value", + cell: ({ row }) => `$${parseFloat(row.getValue("currentValue")).toLocaleString()}` + }, + { + accessorKey: "isActive", + header: "Status", + cell: ({ row }) => ( + + {row.getValue("isActive") ? "Active" : "Inactive"} + + ) + } + ]; + + const expenseColumns: ColumnDef[] = [ + { accessorKey: "description", header: "Description" }, + { accessorKey: "category", header: "Category" }, + { + accessorKey: "amount", + header: "Amount", + cell: ({ row }) => `$${parseFloat(row.getValue("amount")).toLocaleString()}` + }, + { accessorKey: "date", header: "Date" }, + { + accessorKey: "isRecurring", + header: "Recurring", + cell: ({ row }) => (row.getValue("isRecurring") ? "Yes" : "No") + } + ]; + + const incomeColumns: ColumnDef[] = [ + { accessorKey: "source", header: "Source" }, + { accessorKey: "frequency", header: "Frequency" }, + { + accessorKey: "amount", + header: "Amount", + cell: ({ row }) => `$${parseFloat(row.getValue("amount")).toLocaleString()}` + }, + { accessorKey: "date", header: "Date" }, + { + accessorKey: "isRecurring", + header: "Recurring", + cell: ({ row }) => (row.getValue("isRecurring") ? "Yes" : "No") + } + ]; + + if (loading) { + return ( +
+
+
+ ); + } + return (
- -
- -
- + {/* Financial Summary Cards */} + + + {/* Main Content Tabs */} + + + Overview + Assets + Investments + Expenses + Income + + + {/* Overview Tab */} + +
+ + + + + Recent Assets + + Your most valuable possessions + + + {assets.length > 0 ? ( +
+ {assets.slice(0, 3).map(asset => ( +
+
+
{asset.name}
+
{asset.type}
+
+
+ ${parseFloat(asset.value).toLocaleString()} +
+
+ ))} +
+ ) : ( +
+

No assets added yet

+
+ )} + + + Add Asset + + } + onSubmit={(data) => handleCreate("asset", data)} + /> +
+
+ + + + + + Recent Investments + + Your investment portfolio + + + {investments.length > 0 ? ( +
+ {investments.slice(0, 3).map(investment => ( +
+
+
{investment.name}
+
+ {investment.tickerSymbol && `(${investment.tickerSymbol}) `} + {investment.type} +
+
+
+
+ ${parseFloat(investment.currentValue).toLocaleString()} +
+
+ invested: ${parseFloat(investment.amount).toLocaleString()} +
+
+
+ ))} +
+ ) : ( +
+

No investments added yet

+
+ )} + + + Add Investment + + } + onSubmit={(data) => handleCreate("investment", data)} + /> +
+
+
+ +
+ + + + + Recent Expenses + + Your spending activity + + + {expenses.length > 0 ? ( +
+ {expenses.slice(0, 3).map(expense => ( +
+
+
{expense.description}
+
{expense.category}
+
+
+ -${parseFloat(expense.amount).toLocaleString()} +
+
+ ))} +
+ ) : ( +
+

No expenses added yet

+
+ )} + + + Add Expense + + } + onSubmit={(data) => handleCreate("expense", data)} + /> +
+
+ + + + + + Recent Income + + Your earnings + + + {income.length > 0 ? ( +
+ {income.slice(0, 3).map(incomeItem => ( +
+
+
{incomeItem.source}
+
{incomeItem.frequency}
+
+
+ +${parseFloat(incomeItem.amount).toLocaleString()} +
+
+ ))} +
+ ) : ( +
+

No income records added yet

+
+ )} + + + Add Income + + } + onSubmit={(data) => handleCreate("income", data)} + /> +
+
+
+
+ + {/* Assets Tab */} + + + +
+ Assets Management + + Manage your assets including real estate, vehicles, and valuables + +
+ + + Add New Asset + + } + onSubmit={(data) => handleCreate("asset", data)} + /> +
+ + handleDelete("asset", id)} + /> + +
+
+ + {/* Investments Tab */} + + + +
+ Investments Management + + Manage your investment portfolio including stocks, bonds, and crypto + +
+ + + Add New Investment + + } + onSubmit={(data) => handleCreate("investment", data)} + /> +
+ + handleDelete("investment", id)} + /> + +
+
+ + {/* Expenses Tab */} + + + +
+ Expenses Management + + Track and categorize your spending habits + +
+ + + Add New Expense + + } + onSubmit={(data) => handleCreate("expense", data)} + /> +
+ + handleDelete("expense", id)} + /> + +
+
+ + {/* Income Tab */} + + + +
+ Income Management + + Track your income sources and earnings + +
+ + + Add New Income + + } + onSubmit={(data) => handleCreate("income", data)} + /> +
+ + handleDelete("income", id)} + /> + +
+
+
- ) + ); } \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index e7ebec9..5e8bdca 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -10,6 +10,12 @@ import { Globe, Palette, Package, + Landmark, + TrendingUp, + Wallet, + DollarSign, + PiggyBank, + BarChart3, } from "lucide-react"; import { ThemeToggle } from "@/components/theme-toggle"; import { AuthButtons, HeroAuthButtons } from "@/components/auth-buttons"; @@ -28,19 +34,15 @@ export default function Home() {
- CodeGuide Logo -

- CodeGuide Starter +
+ +
+

+ Deepseek Finance

- A modern full-stack TypeScript starter with authentication, database, and UI components + Take control of your financial future. Track assets, investments, expenses, and income in one powerful platform.

@@ -49,122 +51,124 @@ export default function Home() {
{/* Project Overview */}
-
🚀
-
Modern Full-Stack Starter
+
💰
+
Comprehensive Financial Management
- This project includes everything you need to build a modern web application with TypeScript, - authentication, database integration, and a beautiful UI component library. + Manage your entire financial life in one place. Track assets, monitor investments, + analyze expenses, and optimize your income with powerful analytics and insights.
- {/* Tech Stack Grid */} + {/* Features Grid */}
- {/* Frontend */} + {/* Assets Tracking */}
- -

Frontend

+ +

Assets Tracking

    -
  • • Next.js 15 - React framework with App Router
  • -
  • • React 19 - Latest React with concurrent features
  • -
  • • TypeScript - Type-safe development
  • -
  • • Turbopack - Fast bundling and dev server
  • +
  • • Track real estate, vehicles, and valuables
  • +
  • • Monitor asset appreciation over time
  • +
  • • Categorize by type and location
  • +
  • • Calculate total net worth
- {/* UI & Styling */} - + {/* Investment Management */} +
- -

UI & Styling

+ +

Investment Management

    -
  • • Tailwind CSS 4 - Utility-first CSS framework
  • -
  • • Radix UI - Accessible component primitives
  • -
  • • Lucide Icons - Beautiful icon library
  • -
  • • Dark Mode - Built-in theme switching
  • +
  • • Monitor stocks, bonds, and crypto
  • +
  • • Track portfolio performance
  • +
  • • Set investment goals
  • +
  • • Analyze returns and dividends
- {/* Authentication */} - + {/* Expense Tracking */} +
- -

Authentication

+ +

Expense Tracking

    -
  • • Better Auth - Modern auth solution
  • -
  • • Session Management - Secure user sessions
  • -
  • • Type Safety - Fully typed auth hooks
  • -
  • • Multiple Providers - Social login support
  • +
  • • Categorize spending habits
  • +
  • • Set monthly budgets
  • +
  • • Identify cost-saving opportunities
  • +
  • • Track recurring payments
- {/* Database */} + {/* Income Management */}
- -

Database

+ +

Income Management

    -
  • • PostgreSQL - Robust relational database
  • -
  • • Drizzle ORM - Type-safe database toolkit
  • -
  • • Docker Setup - Containerized development
  • -
  • • Migrations - Schema version control
  • +
  • • Track multiple income sources
  • +
  • • Monitor salary and bonuses
  • +
  • • Analyze income trends
  • +
  • • Plan for tax optimization
- {/* Development */} + {/* Savings Goals */}
- -

Development

+ +

Savings Goals

    -
  • • ESLint - Code linting and formatting
  • -
  • • Hot Reload - Instant development feedback
  • -
  • • Docker - Consistent dev environment
  • -
  • • npm Scripts - Automated workflows
  • +
  • • Set financial targets
  • +
  • • Track progress towards goals
  • +
  • • Automate savings plans
  • +
  • • Celebrate milestones
- {/* Components */} + {/* Analytics & Reports */}
- -

Components

+ +

Analytics & Reports

    -
  • • Form Handling - React Hook Form + Zod
  • -
  • • Data Visualization - Recharts integration
  • -
  • • Date Pickers - Beautiful date components
  • -
  • • Notifications - Toast and alert systems
  • +
  • • Visual financial dashboards
  • +
  • • Customizable reports
  • +
  • • Historical trend analysis
  • +
  • • Export to PDF/CSV
- {/* Getting Started */} - + {/* Get Started Today */} +

- - Quick Start + + Start Your Financial Journey

-

Development

-
-
npm install
-
npm run db:dev
-
npm run dev
+

New to Deepseek Finance?

+
+
• Sign up for free account
+
• Connect your financial data
+
• Set up your first budget
+
• Start tracking immediately
-

Production

-
-
npm run build
-
npm run start
-
npm run docker:up
+

Existing User?

+
+
• Sign in to your dashboard
+
• Review your financial health
+
• Update recent transactions
+
• Check progress on goals
diff --git a/components/finance-data-table.tsx b/components/finance-data-table.tsx new file mode 100644 index 0000000..6c18822 --- /dev/null +++ b/components/finance-data-table.tsx @@ -0,0 +1,235 @@ +"use client"; + +import { useState } from "react"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, + getPaginationRowModel, + getSortedRowModel, + SortingState, + ColumnFiltersState, + getFilteredRowModel, +} from "@tanstack/react-table"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { ChevronLeft, ChevronRight, Settings, Search, Edit, Trash2 } from "lucide-react"; + +interface FinanceDataTableProps { + columns: ColumnDef[]; + data: TData[]; + title: string; + description?: string; + onEdit?: (item: TData) => void; + onDelete?: (id: string) => void; +} + +export function FinanceDataTable({ + columns, + data, + title, + description, + onEdit, + onDelete, +}: FinanceDataTableProps) { + const [sorting, setSorting] = useState([]); + const [columnFilters, setColumnFilters] = useState([]); + const [rowSelection, setRowSelection] = useState({}); + + // Add action columns if onEdit or onDelete are provided + const tableColumns = [...columns]; + + if (onEdit || onDelete) { + tableColumns.push({ + id: "actions", + header: "Actions", + cell: ({ row }) => ( +
+ {onEdit && ( + + )} + {onDelete && ( + + )} +
+ ), + }); + } + + const table = useReactTable({ + data, + columns: tableColumns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + rowSelection, + }, + }); + + return ( +
+
+
+

{title}

+ {description && ( +

{description}

+ )} +
+ +
+
+ + + table.getColumn("name")?.setFilterValue(event.target.value) + } + className="pl-8 w-64" + /> +
+ + + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ); + })} + + +
+
+ +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results found. + + + )} + +
+
+ +
+
+ {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected. +
+ +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/components/finance-form-modal.tsx b/components/finance-form-modal.tsx new file mode 100644 index 0000000..f0467a4 --- /dev/null +++ b/components/finance-form-modal.tsx @@ -0,0 +1,280 @@ +"use client"; + +import { useState } from "react"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Plus } from "lucide-react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { Checkbox } from "@/components/ui/checkbox"; + +interface FinanceFormModalProps { + type: "asset" | "investment" | "expense" | "income"; + trigger?: React.ReactNode; + onSubmit: (data: any) => Promise; + defaultValues?: any; +} + +const formSchemas = { + asset: z.object({ + name: z.string().min(1, "Name is required"), + type: z.enum(["cash", "real_estate", "vehicle", "jewelry", "electronics", "other"]), + value: z.number().positive("Value must be positive"), + purchaseDate: z.string().optional(), + description: z.string().optional(), + }), + investment: z.object({ + name: z.string().min(1, "Name is required"), + type: z.enum(["stocks", "bonds", "mutual_funds", "crypto", "real_estate", "retirement", "other"]), + amount: z.number().positive("Amount must be positive"), + currentValue: z.number().positive("Current value must be positive"), + purchaseDate: z.string(), + tickerSymbol: z.string().optional(), + description: z.string().optional(), + isActive: z.boolean().default(true), + }), + expense: z.object({ + description: z.string().min(1, "Description is required"), + amount: z.number().positive("Amount must be positive"), + category: z.enum(["housing", "food", "transportation", "utilities", "healthcare", "entertainment", "education", "clothing", "personal_care", "debt", "savings", "gifts", "other"]), + date: z.string(), + isRecurring: z.boolean().default(false), + recurringFrequency: z.string().optional(), + notes: z.string().optional(), + }), + income: z.object({ + source: z.string().min(1, "Source is required"), + amount: z.number().positive("Amount must be positive"), + frequency: z.enum(["weekly", "biweekly", "monthly", "quarterly", "yearly", "one_time"]), + date: z.string(), + isRecurring: z.boolean().default(true), + description: z.string().optional(), + }), +}; + +const typeLabels = { + asset: "Asset", + investment: "Investment", + expense: "Expense", + income: "Income", +}; + +const typeOptions = { + asset: [ + { value: "cash", label: "Cash" }, + { value: "real_estate", label: "Real Estate" }, + { value: "vehicle", label: "Vehicle" }, + { value: "jewelry", label: "Jewelry" }, + { value: "electronics", label: "Electronics" }, + { value: "other", label: "Other" }, + ], + investment: [ + { value: "stocks", label: "Stocks" }, + { value: "bonds", label: "Bonds" }, + { value: "mutual_funds", label: "Mutual Funds" }, + { value: "crypto", label: "Cryptocurrency" }, + { value: "real_estate", label: "Real Estate" }, + { value: "retirement", label: "Retirement" }, + { value: "other", label: "Other" }, + ], + expense: [ + { value: "housing", label: "Housing" }, + { value: "food", label: "Food" }, + { value: "transportation", label: "Transportation" }, + { value: "utilities", label: "Utilities" }, + { value: "healthcare", label: "Healthcare" }, + { value: "entertainment", label: "Entertainment" }, + { value: "education", label: "Education" }, + { value: "clothing", label: "Clothing" }, + { value: "personal_care", label: "Personal Care" }, + { value: "debt", label: "Debt" }, + { value: "savings", label: "Savings" }, + { value: "gifts", label: "Gifts" }, + { value: "other", label: "Other" }, + ], + income: [ + { value: "weekly", label: "Weekly" }, + { value: "biweekly", label: "Bi-weekly" }, + { value: "monthly", label: "Monthly" }, + { value: "quarterly", label: "Quarterly" }, + { value: "yearly", label: "Yearly" }, + { value: "one_time", label: "One-time" }, + ], +}; + +export function FinanceFormModal({ + type, + trigger, + onSubmit, + defaultValues, +}: FinanceFormModalProps) { + const [open, setOpen] = useState(false); + const [loading, setLoading] = useState(false); + + const form = useForm({ + resolver: zodResolver(formSchemas[type]), + defaultValues: defaultValues || { + isRecurring: false, + isActive: true, + }, + }); + + const handleSubmit = async (data: any) => { + try { + setLoading(true); + await onSubmit(data); + setOpen(false); + form.reset(); + } catch (error) { + console.error("Error submitting form:", error); + } finally { + setLoading(false); + } + }; + + return ( + + + {trigger || ( + + )} + + + + + {defaultValues ? "Edit" : "Add New"} {typeLabels[type]} + + + +
+ + {type === "asset" && ( + <> + ( + + Asset Name + + + + + + )} + /> + ( + + Asset Type + + + + )} + /> + ( + + Value + + field.onChange(parseFloat(e.target.value))} + /> + + + + )} + /> + ( + + Purchase Date (Optional) + + + + + + )} + /> + ( + + Description (Optional) + +