diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 43592e1..dbec404 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,6 +8,7 @@ import { DashboardPage } from '@/pages/DashboardPage' import { CategoriesPage } from '@/pages/CategoriesPage' import { SettingsPage } from '@/pages/SettingsPage' import { TemplatePage } from '@/pages/TemplatePage' +import { QuickAddPage } from '@/pages/QuickAddPage' import '@/i18n' export default function App() { @@ -37,6 +38,7 @@ export default function App() { } /> } /> } /> + } /> } /> diff --git a/frontend/src/components/AppLayout.tsx b/frontend/src/components/AppLayout.tsx index 28ea2a3..75753a2 100644 --- a/frontend/src/components/AppLayout.tsx +++ b/frontend/src/components/AppLayout.tsx @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next' import { Link, useLocation } from 'react-router-dom' -import { LayoutDashboard, Tags, FileText, Settings, LogOut } from 'lucide-react' +import { LayoutDashboard, Tags, FileText, Settings, LogOut, Zap } from 'lucide-react' import { Sidebar, SidebarContent, @@ -31,6 +31,7 @@ export function AppLayout({ auth, children }: Props) { { path: '/', label: t('nav.dashboard'), icon: LayoutDashboard }, { path: '/categories', label: t('nav.categories'), icon: Tags }, { path: '/template', label: t('nav.template'), icon: FileText }, + { path: '/quick-add', label: t('nav.quickAdd'), icon: Zap }, { path: '/settings', label: t('nav.settings'), icon: Settings }, ] diff --git a/frontend/src/hooks/useQuickAdd.ts b/frontend/src/hooks/useQuickAdd.ts new file mode 100644 index 0000000..31f5a8e --- /dev/null +++ b/frontend/src/hooks/useQuickAdd.ts @@ -0,0 +1,40 @@ +import { useState, useEffect } from 'react' +import { quickAdd as quickAddApi, type QuickAddItem } from '@/lib/api' + +export function useQuickAdd() { + const [items, setItems] = useState([]) + const [loading, setLoading] = useState(true) + + const fetchItems = async () => { + const data = await quickAddApi.list() + setItems(data ?? []) + } + + useEffect(() => { + const init = async () => { + try { + await fetchItems() + } finally { + setLoading(false) + } + } + init() + }, []) + + const addItem = async (name: string, icon: string) => { + await quickAddApi.create({ name, icon }) + await fetchItems() + } + + const updateItem = async (id: string, name: string, icon: string, sortOrder: number) => { + await quickAddApi.update(id, { name, icon, sort_order: sortOrder }) + await fetchItems() + } + + const removeItem = async (id: string) => { + await quickAddApi.delete(id) + await fetchItems() + } + + return { items, loading, addItem, updateItem, removeItem } +} diff --git a/frontend/src/i18n/de.json b/frontend/src/i18n/de.json index f5440c9..e19edfd 100644 --- a/frontend/src/i18n/de.json +++ b/frontend/src/i18n/de.json @@ -6,6 +6,7 @@ "dashboard": "Dashboard", "categories": "Kategorien", "template": "Vorlage", + "quickAdd": "Schnellzugriff", "settings": "Einstellungen", "logout": "Abmelden" }, @@ -91,6 +92,21 @@ "selectCategory": "Kategorie auswaehlen", "selectTier": "Typ auswaehlen" }, + "quickAdd": { + "title": "Schnellzugriff-Bibliothek", + "name": "Name", + "icon": "Symbol", + "addItem": "Hinzufuegen", + "noItems": "Keine gespeicherten Eintraege", + "noItemsHint": "Speichere hier haeufig genutzte Einmal-Kategorien fuer schnellen Zugriff.", + "editItem": "Bearbeiten", + "deleteItem": "Entfernen", + "save": "Speichern", + "cancel": "Abbrechen", + "addOneOff": "Schnellzugriff", + "emptyPicker": "Keine gespeicherten Eintraege", + "goToLibrary": "Bibliothek verwalten" + }, "common": { "save": "Speichern", "cancel": "Abbrechen", diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 2bb48be..5f8e26d 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -6,6 +6,7 @@ "dashboard": "Dashboard", "categories": "Categories", "template": "Template", + "quickAdd": "Quick Add", "settings": "Settings", "logout": "Logout" }, @@ -91,6 +92,21 @@ "selectCategory": "Select category", "selectTier": "Select tier" }, + "quickAdd": { + "title": "Quick-Add Library", + "name": "Name", + "icon": "Icon", + "addItem": "Add Item", + "noItems": "No saved items", + "noItemsHint": "Save your frequently-used one-off categories here for quick access.", + "editItem": "Edit", + "deleteItem": "Remove", + "save": "Save", + "cancel": "Cancel", + "addOneOff": "Quick Add", + "emptyPicker": "No saved items", + "goToLibrary": "Manage library" + }, "common": { "save": "Save", "cancel": "Cancel", diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index be1ebc5..b0fbe77 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -108,6 +108,16 @@ export interface TemplateDetail { items: TemplateItem[] } +export interface QuickAddItem { + id: string + user_id: string + name: string + icon: string + sort_order: number + created_at: string + updated_at: string +} + export interface BudgetDetail extends Budget { items: BudgetItem[] totals: { @@ -164,6 +174,17 @@ export const template = { request('/template/items/reorder', { method: 'PUT', body: JSON.stringify({ items }) }), } +// Quick Add Library +export const quickAdd = { + list: () => request('/quick-add'), + create: (data: { name: string; icon: string }) => + request('/quick-add', { method: 'POST', body: JSON.stringify(data) }), + update: (id: string, data: { name: string; icon: string; sort_order: number }) => + request(`/quick-add/${id}`, { method: 'PUT', body: JSON.stringify(data) }), + delete: (id: string) => + request(`/quick-add/${id}`, { method: 'DELETE' }), +} + // Settings export const settings = { get: () => request('/settings'), diff --git a/frontend/src/pages/QuickAddPage.tsx b/frontend/src/pages/QuickAddPage.tsx new file mode 100644 index 0000000..d6b27ee --- /dev/null +++ b/frontend/src/pages/QuickAddPage.tsx @@ -0,0 +1,203 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import { Skeleton } from '@/components/ui/skeleton' +import { EmptyState } from '@/components/EmptyState' +import { Zap, Pencil, Trash2, Check, X } from 'lucide-react' +import { useQuickAdd } from '@/hooks/useQuickAdd' + +export function QuickAddPage() { + const { t } = useTranslation() + const { items, loading, addItem, updateItem, removeItem } = useQuickAdd() + + const [newName, setNewName] = useState('') + const [newIcon, setNewIcon] = useState('') + const [adding, setAdding] = useState(false) + + const [editId, setEditId] = useState(null) + const [editName, setEditName] = useState('') + const [editIcon, setEditIcon] = useState('') + const [saving, setSaving] = useState(false) + + const handleAdd = async () => { + if (!newName.trim()) return + setAdding(true) + try { + await addItem(newName.trim(), newIcon.trim()) + setNewName('') + setNewIcon('') + } finally { + setAdding(false) + } + } + + const startEdit = (id: string, name: string, icon: string) => { + setEditId(id) + setEditName(name) + setEditIcon(icon) + } + + const cancelEdit = () => { + setEditId(null) + setEditName('') + setEditIcon('') + } + + const handleSaveEdit = async (item: { id: string; sort_order: number }) => { + if (!editName.trim()) return + setSaving(true) + try { + await updateItem(item.id, editName.trim(), editIcon.trim(), item.sort_order) + setEditId(null) + setEditName('') + setEditIcon('') + } finally { + setSaving(false) + } + } + + if (loading) { + return ( + + + + + + + + + + + + + ) + } + + return ( + + {t('quickAdd.title')} + + + + {t('quickAdd.title')} + + + {/* Add item form */} + + + setNewName(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleAdd()} + /> + + + setNewIcon(e.target.value)} + /> + + + + {t('quickAdd.addItem')} + + + + {/* Items table or empty state */} + {items.length === 0 ? ( + + ) : ( + + + + {t('quickAdd.name')} + {t('quickAdd.icon')} + {/* actions */} + + + + {items.map((item) => ( + + + {editId === item.id ? ( + setEditName(e.target.value)} + className="h-8" + /> + ) : ( + {item.name} + )} + + + {editId === item.id ? ( + setEditIcon(e.target.value)} + className="h-8 w-20" + /> + ) : ( + {item.icon} + )} + + + {editId === item.id ? ( + + handleSaveEdit(item)} + aria-label={t('quickAdd.save')} + > + + + + + + + ) : ( + + startEdit(item.id, item.name, item.icon)} + aria-label={t('quickAdd.editItem')} + > + + + removeItem(item.id)} + aria-label={t('quickAdd.deleteItem')} + > + + + + )} + + + ))} + + + )} + + + + ) +}