feat(07-02): add QuickAddPicker component and wire into DashboardPage
- Created QuickAddPicker component using DropdownMenu (no Popover available) - Picker fetches quick-add library on mount and shows items with icon + name - On item select: finds or creates matching category, then creates one_off budget item - Empty state shows link to /quick-add management page - Loading spinner on selected item while creating - Wired QuickAddPicker into DashboardPage toolbar next to Create Budget button
This commit is contained in:
114
frontend/src/components/QuickAddPicker.tsx
Normal file
114
frontend/src/components/QuickAddPicker.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Zap } from 'lucide-react'
|
||||
import { quickAdd as quickAddApi, categories as categoriesApi, budgetItems, type QuickAddItem } from '@/lib/api'
|
||||
|
||||
interface Props {
|
||||
budgetId: string
|
||||
onItemAdded: () => void
|
||||
}
|
||||
|
||||
export function QuickAddPicker({ budgetId, onItemAdded }: Props) {
|
||||
const { t } = useTranslation()
|
||||
const [items, setItems] = useState<QuickAddItem[]>([])
|
||||
const [loadingItems, setLoadingItems] = useState(true)
|
||||
const [creatingId, setCreatingId] = useState<string | null>(null)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
quickAddApi
|
||||
.list()
|
||||
.then((data) => setItems(data ?? []))
|
||||
.catch(() => setItems([]))
|
||||
.finally(() => setLoadingItems(false))
|
||||
}, [])
|
||||
|
||||
const handleSelect = async (item: QuickAddItem) => {
|
||||
if (creatingId) return
|
||||
setCreatingId(item.id)
|
||||
try {
|
||||
// Find or create a matching category for the quick-add item
|
||||
const allCategories = await categoriesApi.list()
|
||||
let categoryId: string
|
||||
|
||||
const existing = allCategories.find(
|
||||
(c) => c.name.toLowerCase() === item.name.toLowerCase()
|
||||
)
|
||||
|
||||
if (existing) {
|
||||
categoryId = existing.id
|
||||
} else {
|
||||
const created = await categoriesApi.create({
|
||||
name: item.name,
|
||||
type: 'variable_expense',
|
||||
icon: item.icon,
|
||||
})
|
||||
categoryId = created.id
|
||||
}
|
||||
|
||||
await budgetItems.create(budgetId, {
|
||||
category_id: categoryId,
|
||||
item_tier: 'one_off',
|
||||
})
|
||||
|
||||
setOpen(false)
|
||||
onItemAdded()
|
||||
} finally {
|
||||
setCreatingId(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<Zap className="size-4 mr-1" />
|
||||
{t('quickAdd.addOneOff')}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-56">
|
||||
{loadingItems ? (
|
||||
<div className="px-2 py-2 text-sm text-muted-foreground">{t('common.loading')}</div>
|
||||
) : items.length === 0 ? (
|
||||
<div className="px-2 py-2 text-sm text-muted-foreground">
|
||||
<p className="mb-1">{t('quickAdd.emptyPicker')}</p>
|
||||
<Link
|
||||
to="/quick-add"
|
||||
className="text-xs underline text-primary"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{t('quickAdd.goToLibrary')}
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
items.map((item) => (
|
||||
<DropdownMenuItem
|
||||
key={item.id}
|
||||
disabled={creatingId === item.id}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault()
|
||||
handleSelect(item)
|
||||
}}
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
>
|
||||
{creatingId === item.id ? (
|
||||
<span className="size-4 animate-spin border-2 border-current border-t-transparent rounded-full inline-block" />
|
||||
) : (
|
||||
<span>{item.icon}</span>
|
||||
)}
|
||||
<span>{item.name}</span>
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { EmptyState } from '@/components/EmptyState'
|
||||
import { useBudgets } from '@/hooks/useBudgets'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { budgetItems as budgetItemsApi } from '@/lib/api'
|
||||
import { QuickAddPicker } from '@/components/QuickAddPicker'
|
||||
import { FolderOpen } from 'lucide-react'
|
||||
import { palette } from '@/lib/palette'
|
||||
|
||||
@@ -98,6 +99,12 @@ export function DashboardPage() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button onClick={() => setShowCreate(true)}>{t('budget.create')}</Button>
|
||||
{current && (
|
||||
<QuickAddPicker
|
||||
budgetId={current.id}
|
||||
onItemAdded={() => selectBudget(current.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showCreate && (
|
||||
|
||||
Reference in New Issue
Block a user