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
|
carryover_amount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ItemTier = 'fixed' | 'variable' | 'one_off'
|
||||||
|
|
||||||
export interface BudgetItem {
|
export interface BudgetItem {
|
||||||
id: string
|
id: string
|
||||||
budget_id: string
|
budget_id: string
|
||||||
category_id: string
|
category_id: string
|
||||||
category_name: string
|
category_name: string
|
||||||
category_type: CategoryType
|
category_type: CategoryType
|
||||||
|
item_tier: ItemTier
|
||||||
budgeted_amount: number
|
budgeted_amount: number
|
||||||
actual_amount: number
|
actual_amount: number
|
||||||
notes: string
|
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 {
|
export interface BudgetDetail extends Budget {
|
||||||
items: BudgetItem[]
|
items: BudgetItem[]
|
||||||
totals: {
|
totals: {
|
||||||
@@ -116,6 +137,8 @@ export const budgets = {
|
|||||||
delete: (id: string) => request<void>(`/budgets/${id}`, { method: 'DELETE' }),
|
delete: (id: string) => request<void>(`/budgets/${id}`, { method: 'DELETE' }),
|
||||||
copyFrom: (id: string, srcId: string) =>
|
copyFrom: (id: string, srcId: string) =>
|
||||||
request<BudgetDetail>(`/budgets/${id}/copy-from/${srcId}`, { method: 'POST' }),
|
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
|
// Budget Items
|
||||||
@@ -128,6 +151,21 @@ export const budgetItems = {
|
|||||||
request<void>(`/budgets/${budgetId}/items/${itemId}`, { method: 'DELETE' }),
|
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
|
// Settings
|
||||||
export const settings = {
|
export const settings = {
|
||||||
get: () => request<User>('/settings'),
|
get: () => request<User>('/settings'),
|
||||||
|
|||||||
Reference in New Issue
Block a user