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:
2026-03-12 13:39:24 +01:00
parent 411a986c14
commit 8238e07582
2 changed files with 121 additions and 0 deletions

View 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>
)
}

View File

@@ -14,6 +14,7 @@ import { EmptyState } from '@/components/EmptyState'
import { useBudgets } from '@/hooks/useBudgets' import { useBudgets } from '@/hooks/useBudgets'
import { useAuth } from '@/hooks/useAuth' import { useAuth } from '@/hooks/useAuth'
import { budgetItems as budgetItemsApi } from '@/lib/api' import { budgetItems as budgetItemsApi } from '@/lib/api'
import { QuickAddPicker } from '@/components/QuickAddPicker'
import { FolderOpen } from 'lucide-react' import { FolderOpen } from 'lucide-react'
import { palette } from '@/lib/palette' import { palette } from '@/lib/palette'
@@ -98,6 +99,12 @@ export function DashboardPage() {
</SelectContent> </SelectContent>
</Select> </Select>
<Button onClick={() => setShowCreate(true)}>{t('budget.create')}</Button> <Button onClick={() => setShowCreate(true)}>{t('budget.create')}</Button>
{current && (
<QuickAddPicker
budgetId={current.id}
onItemAdded={() => selectBudget(current.id)}
/>
)}
</div> </div>
{showCreate && ( {showCreate && (