diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5af1580..2384edb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -21,6 +21,7 @@ model User { enum AccountType { BANK CREDIT_CARD + INVESTMENT } model Account { diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx index ad6668f..ed91244 100644 --- a/src/app/(app)/dashboard/page.tsx +++ b/src/app/(app)/dashboard/page.tsx @@ -68,7 +68,7 @@ export default async function DashboardPage({ searchParams }: { searchParams: Se if (isCurrentMonth) { const accounts = await prisma.account.findMany({ - where: { userId, isActive: true, type: 'BANK' }, + where: { userId, isActive: true, type: { in: ['BANK', 'INVESTMENT'] } }, select: { id: true, name: true, currentBalanceCents: true }, orderBy: { name: 'asc' }, }) @@ -80,7 +80,7 @@ export default async function DashboardPage({ searchParams }: { searchParams: Se FROM "BalanceSnapshot" bs JOIN "Account" a ON bs."accountId" = a.id WHERE a."userId" = ${userId} - AND a.type = 'BANK' + AND a.type IN ('BANK', 'INVESTMENT') AND bs.year = ${year} AND bs.month = ${month} ` diff --git a/src/app/api/accounts/[id]/record-value/route.ts b/src/app/api/accounts/[id]/record-value/route.ts new file mode 100644 index 0000000..3187734 --- /dev/null +++ b/src/app/api/accounts/[id]/record-value/route.ts @@ -0,0 +1,44 @@ +import { NextResponse } from 'next/server' +import { z } from 'zod' +import { auth } from '@/lib/auth' +import { prisma } from '@/lib/prisma' + +const schema = z.object({ + valueCents: z.number().int(), + date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be YYYY-MM-DD'), +}) + +export async function POST( + req: Request, + { params }: { params: Promise<{ id: string }> }, +) { + const session = await auth() + if (!session?.user?.id) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + + const { id } = await params + const account = await prisma.account.findFirst({ + where: { id, userId: session.user.id, type: 'INVESTMENT' }, + }) + if (!account) return NextResponse.json({ error: 'Account not found' }, { status: 404 }) + + const result = schema.safeParse(await req.json()) + if (!result.success) return NextResponse.json({ error: result.error.flatten() }, { status: 400 }) + + const { valueCents, date } = result.data + const d = new Date(date + 'T12:00:00') + const year = d.getFullYear() + const month = d.getMonth() + 1 + + await prisma.account.update({ + where: { id }, + data: { currentBalanceCents: valueCents }, + }) + + await prisma.balanceSnapshot.upsert({ + where: { accountId_year_month: { accountId: id, year, month } }, + update: { balanceCents: valueCents, computedAt: new Date() }, + create: { accountId: id, year, month, balanceCents: valueCents }, + }) + + return NextResponse.json({ ok: true }) +} diff --git a/src/components/accounts/AccountBadge.tsx b/src/components/accounts/AccountBadge.tsx index 436160e..1caf561 100644 --- a/src/components/accounts/AccountBadge.tsx +++ b/src/components/accounts/AccountBadge.tsx @@ -2,9 +2,7 @@ import { Badge } from '@/components/ui/badge' import { AccountType } from '@/generated/prisma/client' export function AccountBadge({ type }: { type: AccountType }) { - return type === 'BANK' ? ( - Bank - ) : ( - Credit Card - ) + if (type === 'BANK') return Bank + if (type === 'INVESTMENT') return Investment + return Credit Card } diff --git a/src/components/accounts/AccountCard.tsx b/src/components/accounts/AccountCard.tsx index fa276c0..b404319 100644 --- a/src/components/accounts/AccountCard.tsx +++ b/src/components/accounts/AccountCard.tsx @@ -9,14 +9,16 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' -import { MoreHorizontal, Pencil, Trash2, EyeOff, Eye } from 'lucide-react' +import { MoreHorizontal, Pencil, Trash2, EyeOff, Eye, TrendingUp } from 'lucide-react' import { AccountBadge } from './AccountBadge' import { CreateAccountDialog } from './CreateAccountDialog' +import { RecordValueDialog } from './RecordValueDialog' import { formatCents } from '@/lib/utils/currency' export function AccountCard({ account }: { account: Account }) { const router = useRouter() const [editOpen, setEditOpen] = useState(false) + const [recordOpen, setRecordOpen] = useState(false) async function handleToggleActive() { await fetch(`/api/accounts/${account.id}`, { @@ -56,9 +58,13 @@ export function AccountCard({ account }: { account: Account }) { setEditOpen(true)}> - - Edit + Edit + {account.type === 'INVESTMENT' && ( + setRecordOpen(true)}> + Record value + + )} {account.isActive ? <>Deactivate @@ -87,11 +93,15 @@ export function AccountCard({ account }: { account: Account }) { - + + {account.type === 'INVESTMENT' && ( + + )} ) } diff --git a/src/components/accounts/CreateAccountDialog.tsx b/src/components/accounts/CreateAccountDialog.tsx index 7f01d3b..ae05982 100644 --- a/src/components/accounts/CreateAccountDialog.tsx +++ b/src/components/accounts/CreateAccountDialog.tsx @@ -93,6 +93,7 @@ export function CreateAccountDialog({ open, onOpenChange, account }: Props) { Bank Credit Card + Investment diff --git a/src/components/accounts/RecordValueDialog.tsx b/src/components/accounts/RecordValueDialog.tsx new file mode 100644 index 0000000..225f144 --- /dev/null +++ b/src/components/accounts/RecordValueDialog.tsx @@ -0,0 +1,77 @@ +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Button } from '@/components/ui/button' + +interface Props { + open: boolean + onOpenChange: (open: boolean) => void + accountId: string + accountName: string +} + +export function RecordValueDialog({ open, onOpenChange, accountId, accountName }: Props) { + const router = useRouter() + const [value, setValue] = useState('') + const [date, setDate] = useState(() => new Date().toISOString().split('T')[0]) + const [saving, setSaving] = useState(false) + const [error, setError] = useState('') + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + const num = parseFloat(value.replace(/[$,]/g, '')) + if (isNaN(num)) { setError('Enter a valid dollar amount'); return } + setError('') + setSaving(true) + const res = await fetch(`/api/accounts/${accountId}/record-value`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ valueCents: Math.round(num * 100), date }), + }) + setSaving(false) + if (!res.ok) { setError('Failed to save. Please try again.'); return } + onOpenChange(false) + router.refresh() + } + + return ( + + + + Record portfolio value — {accountName} + +
+
+ + setValue(e.target.value)} + required + /> +
+
+ + setDate(e.target.value)} + required + /> +
+ {error &&

{error}

} + + + + +
+
+
+ ) +} diff --git a/src/lib/validations/account.ts b/src/lib/validations/account.ts index e70e851..d75e4d1 100644 --- a/src/lib/validations/account.ts +++ b/src/lib/validations/account.ts @@ -3,7 +3,7 @@ import { z } from 'zod' export const createAccountSchema = z.object({ name: z.string().min(1, 'Name is required').max(100), institution: z.string().max(100).optional(), - type: z.enum(['BANK', 'CREDIT_CARD']), + type: z.enum(['BANK', 'CREDIT_CARD', 'INVESTMENT']), currency: z.string().length(3).default('USD'), })