feat(06-01): add template API client functions and useTemplate hook
- Add ItemTier type and TemplateItem/TemplateDetail interfaces to api.ts - Add item_tier field to BudgetItem interface - Add template API object with get/updateName/addItem/updateItem/deleteItem/reorder - Add generate function to budgets API object - Create useTemplate hook with CRUD operations and reorder logic
This commit is contained in:
98
frontend/src/hooks/useTemplate.ts
Normal file
98
frontend/src/hooks/useTemplate.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import {
|
||||
template as templateApi,
|
||||
categories as categoriesApi,
|
||||
type TemplateDetail,
|
||||
type TemplateItem,
|
||||
type Category,
|
||||
type ItemTier,
|
||||
} from '@/lib/api'
|
||||
|
||||
export function useTemplate() {
|
||||
const [templateDetail, setTemplateDetail] = useState<TemplateDetail | null>(null)
|
||||
const [cats, setCats] = useState<Category[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const fetchTemplate = async () => {
|
||||
const data = await templateApi.get()
|
||||
setTemplateDetail(data)
|
||||
}
|
||||
|
||||
const fetchCategories = async () => {
|
||||
const data = await categoriesApi.list()
|
||||
setCats(data)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
try {
|
||||
await Promise.all([fetchTemplate(), fetchCategories()])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
init()
|
||||
}, [])
|
||||
|
||||
const addItem = async (data: {
|
||||
category_id: string
|
||||
item_tier: ItemTier
|
||||
budgeted_amount?: number
|
||||
}) => {
|
||||
await templateApi.addItem(data)
|
||||
await fetchTemplate()
|
||||
}
|
||||
|
||||
const removeItem = async (itemId: string) => {
|
||||
await templateApi.deleteItem(itemId)
|
||||
await fetchTemplate()
|
||||
}
|
||||
|
||||
const moveItem = async (itemId: string, direction: 'up' | 'down') => {
|
||||
if (!templateDetail) return
|
||||
const items = [...templateDetail.items].sort((a, b) => a.sort_order - b.sort_order)
|
||||
const idx = items.findIndex((i) => i.id === itemId)
|
||||
if (idx === -1) return
|
||||
const swapIdx = direction === 'up' ? idx - 1 : idx + 1
|
||||
if (swapIdx < 0 || swapIdx >= items.length) return
|
||||
|
||||
const current = items[idx]
|
||||
const swap = items[swapIdx]
|
||||
const reordered: { id: string; sort_order: number }[] = items.map((item) => {
|
||||
if (item.id === current.id) return { id: item.id, sort_order: swap.sort_order }
|
||||
if (item.id === swap.id) return { id: item.id, sort_order: current.sort_order }
|
||||
return { id: item.id, sort_order: item.sort_order }
|
||||
})
|
||||
|
||||
await templateApi.reorder(reordered)
|
||||
await fetchTemplate()
|
||||
}
|
||||
|
||||
const updateItem = async (
|
||||
itemId: string,
|
||||
data: { item_tier?: ItemTier; budgeted_amount?: number }
|
||||
) => {
|
||||
await templateApi.updateItem(itemId, data)
|
||||
await fetchTemplate()
|
||||
}
|
||||
|
||||
return {
|
||||
template: templateDetail,
|
||||
categories: cats,
|
||||
loading,
|
||||
addItem,
|
||||
removeItem,
|
||||
moveItem,
|
||||
updateItem,
|
||||
refetch: fetchTemplate,
|
||||
} as {
|
||||
template: TemplateDetail | null
|
||||
categories: Category[]
|
||||
loading: boolean
|
||||
addItem: (data: { category_id: string; item_tier: ItemTier; budgeted_amount?: number }) => Promise<void>
|
||||
removeItem: (itemId: string) => Promise<void>
|
||||
moveItem: (itemId: string, direction: 'up' | 'down') => Promise<void>
|
||||
updateItem: (itemId: string, data: { item_tier?: ItemTier; budgeted_amount?: number }) => Promise<void>
|
||||
refetch: () => Promise<void>
|
||||
}
|
||||
}
|
||||
@@ -76,17 +76,38 @@ export interface Budget {
|
||||
carryover_amount: number
|
||||
}
|
||||
|
||||
export type ItemTier = 'fixed' | 'variable' | 'one_off'
|
||||
|
||||
export interface BudgetItem {
|
||||
id: string
|
||||
budget_id: string
|
||||
category_id: string
|
||||
category_name: string
|
||||
category_type: CategoryType
|
||||
item_tier: ItemTier
|
||||
budgeted_amount: number
|
||||
actual_amount: number
|
||||
notes: string
|
||||
}
|
||||
|
||||
export interface TemplateItem {
|
||||
id: string
|
||||
template_id: string
|
||||
category_id: string
|
||||
category_name: string
|
||||
category_type: CategoryType
|
||||
category_icon: string
|
||||
item_tier: ItemTier
|
||||
budgeted_amount: number | null
|
||||
sort_order: number
|
||||
}
|
||||
|
||||
export interface TemplateDetail {
|
||||
id: string | null
|
||||
name: string
|
||||
items: TemplateItem[]
|
||||
}
|
||||
|
||||
export interface BudgetDetail extends Budget {
|
||||
items: BudgetItem[]
|
||||
totals: {
|
||||
@@ -116,6 +137,8 @@ export const budgets = {
|
||||
delete: (id: string) => request<void>(`/budgets/${id}`, { method: 'DELETE' }),
|
||||
copyFrom: (id: string, srcId: string) =>
|
||||
request<BudgetDetail>(`/budgets/${id}/copy-from/${srcId}`, { method: 'POST' }),
|
||||
generate: (data: { month: string; currency: string }) =>
|
||||
request<BudgetDetail>('/budgets/generate', { method: 'POST', body: JSON.stringify(data) }),
|
||||
}
|
||||
|
||||
// Budget Items
|
||||
@@ -128,6 +151,21 @@ export const budgetItems = {
|
||||
request<void>(`/budgets/${budgetId}/items/${itemId}`, { method: 'DELETE' }),
|
||||
}
|
||||
|
||||
// Template
|
||||
export const template = {
|
||||
get: () => request<TemplateDetail>('/template'),
|
||||
updateName: (name: string) =>
|
||||
request<TemplateDetail>('/template', { method: 'PUT', body: JSON.stringify({ name }) }),
|
||||
addItem: (data: { category_id: string; item_tier: ItemTier; budgeted_amount?: number }) =>
|
||||
request<TemplateItem>('/template/items', { method: 'POST', body: JSON.stringify(data) }),
|
||||
updateItem: (itemId: string, data: { item_tier?: ItemTier; budgeted_amount?: number }) =>
|
||||
request<TemplateItem>(`/template/items/${itemId}`, { method: 'PUT', body: JSON.stringify(data) }),
|
||||
deleteItem: (itemId: string) =>
|
||||
request<void>(`/template/items/${itemId}`, { method: 'DELETE' }),
|
||||
reorder: (items: { id: string; sort_order: number }[]) =>
|
||||
request<void>('/template/items/reorder', { method: 'PUT', body: JSON.stringify({ items }) }),
|
||||
}
|
||||
|
||||
// Settings
|
||||
export const settings = {
|
||||
get: () => request<User>('/settings'),
|
||||
|
||||
Reference in New Issue
Block a user