Add INVESTMENT account type with manual portfolio value recording

This commit is contained in:
2026-04-21 11:32:50 -04:00
parent 0ea6a7c698
commit 0cf4612106
8 changed files with 147 additions and 16 deletions

View File

@@ -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' ? (
<Badge variant="secondary">Bank</Badge>
) : (
<Badge variant="outline">Credit Card</Badge>
)
if (type === 'BANK') return <Badge variant="secondary">Bank</Badge>
if (type === 'INVESTMENT') return <Badge variant="secondary" className="bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">Investment</Badge>
return <Badge variant="outline">Credit Card</Badge>
}

View File

@@ -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 }) {
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setEditOpen(true)}>
<Pencil className="h-4 w-4 mr-2" />
Edit
<Pencil className="h-4 w-4 mr-2" />Edit
</DropdownMenuItem>
{account.type === 'INVESTMENT' && (
<DropdownMenuItem onClick={() => setRecordOpen(true)}>
<TrendingUp className="h-4 w-4 mr-2" />Record value
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={handleToggleActive}>
{account.isActive
? <><EyeOff className="h-4 w-4 mr-2" />Deactivate</>
@@ -87,11 +93,15 @@ export function AccountCard({ account }: { account: Account }) {
</CardContent>
</Card>
<CreateAccountDialog
open={editOpen}
onOpenChange={setEditOpen}
account={account}
/>
<CreateAccountDialog open={editOpen} onOpenChange={setEditOpen} account={account} />
{account.type === 'INVESTMENT' && (
<RecordValueDialog
open={recordOpen}
onOpenChange={setRecordOpen}
accountId={account.id}
accountName={account.name}
/>
)}
</>
)
}

View File

@@ -93,6 +93,7 @@ export function CreateAccountDialog({ open, onOpenChange, account }: Props) {
<SelectContent>
<SelectItem value="BANK">Bank</SelectItem>
<SelectItem value="CREDIT_CARD">Credit Card</SelectItem>
<SelectItem value="INVESTMENT">Investment</SelectItem>
</SelectContent>
</Select>
</div>

View File

@@ -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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle>Record portfolio value {accountName}</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4 pt-1">
<div className="space-y-2">
<Label htmlFor="rv-value">Portfolio value</Label>
<Input
id="rv-value"
placeholder="e.g. 12500.00"
value={value}
onChange={(e) => setValue(e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="rv-date">As of date</Label>
<Input
id="rv-date"
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
required
/>
</div>
{error && <p className="text-sm text-destructive">{error}</p>}
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button type="submit" disabled={saving}>{saving ? 'Saving…' : 'Save'}</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}