sum
This commit is contained in:
207
src/hooks/useTemplate.ts
Normal file
207
src/hooks/useTemplate.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { supabase } from "@/lib/supabase"
|
||||
import type { Template, TemplateItem } from "@/lib/types"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Query keys
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const TEMPLATE_KEY = ["template"] as const
|
||||
const TEMPLATE_ITEMS_KEY = ["template-items"] as const
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function getOrCreateTemplate(): Promise<Template> {
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
if (!user) throw new Error("Not authenticated")
|
||||
|
||||
// Try to load the user's existing template first.
|
||||
const { data: existing, error: fetchError } = await supabase
|
||||
.from("templates")
|
||||
.select("*")
|
||||
.eq("user_id", user.id)
|
||||
.maybeSingle()
|
||||
|
||||
if (fetchError) throw fetchError
|
||||
if (existing) return existing as Template
|
||||
|
||||
// No template yet — create one.
|
||||
const { data: created, error: createError } = await supabase
|
||||
.from("templates")
|
||||
.insert({ user_id: user.id, name: "My Monthly Template" })
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (createError) throw createError
|
||||
return created as Template
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main hook
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function useTemplate() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Query: template row (auto-created if absent)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
const templateQuery = useQuery({
|
||||
queryKey: TEMPLATE_KEY,
|
||||
queryFn: getOrCreateTemplate,
|
||||
})
|
||||
|
||||
const templateId = templateQuery.data?.id
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Query: template items with joined category data
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
const itemsQuery = useQuery({
|
||||
queryKey: TEMPLATE_ITEMS_KEY,
|
||||
queryFn: async () => {
|
||||
if (!templateId) return []
|
||||
const { data, error } = await supabase
|
||||
.from("template_items")
|
||||
.select("*, category:categories(*)")
|
||||
.eq("template_id", templateId)
|
||||
.order("sort_order")
|
||||
if (error) throw error
|
||||
return data as TemplateItem[]
|
||||
},
|
||||
enabled: Boolean(templateId),
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Mutation: update template name
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
const updateName = useMutation({
|
||||
mutationFn: async (name: string) => {
|
||||
if (!templateId) throw new Error("Template not loaded")
|
||||
const { data, error } = await supabase
|
||||
.from("templates")
|
||||
.update({ name })
|
||||
.eq("id", templateId)
|
||||
.select()
|
||||
.single()
|
||||
if (error) throw error
|
||||
return data as Template
|
||||
},
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: TEMPLATE_KEY }),
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Mutation: create template item
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
const createItem = useMutation({
|
||||
mutationFn: async (payload: {
|
||||
category_id: string
|
||||
item_tier: TemplateItem["item_tier"]
|
||||
budgeted_amount: number
|
||||
}) => {
|
||||
if (!templateId) throw new Error("Template not loaded")
|
||||
|
||||
// Determine the next sort_order value from the current items list.
|
||||
const currentItems = queryClient.getQueryData<TemplateItem[]>(TEMPLATE_ITEMS_KEY) ?? []
|
||||
const nextOrder =
|
||||
currentItems.length > 0
|
||||
? Math.max(...currentItems.map((i) => i.sort_order)) + 1
|
||||
: 0
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("template_items")
|
||||
.insert({ ...payload, template_id: templateId, sort_order: nextOrder })
|
||||
.select("*, category:categories(*)")
|
||||
.single()
|
||||
if (error) throw error
|
||||
return data as TemplateItem
|
||||
},
|
||||
onSuccess: () =>
|
||||
queryClient.invalidateQueries({ queryKey: TEMPLATE_ITEMS_KEY }),
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Mutation: update template item
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
const updateItem = useMutation({
|
||||
mutationFn: async ({
|
||||
id,
|
||||
...updates
|
||||
}: { id: string } & Partial<
|
||||
Pick<TemplateItem, "item_tier" | "budgeted_amount" | "sort_order">
|
||||
>) => {
|
||||
const { data, error } = await supabase
|
||||
.from("template_items")
|
||||
.update(updates)
|
||||
.eq("id", id)
|
||||
.select("*, category:categories(*)")
|
||||
.single()
|
||||
if (error) throw error
|
||||
return data as TemplateItem
|
||||
},
|
||||
onSuccess: () =>
|
||||
queryClient.invalidateQueries({ queryKey: TEMPLATE_ITEMS_KEY }),
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Mutation: delete template item
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
const deleteItem = useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
const { error } = await supabase
|
||||
.from("template_items")
|
||||
.delete()
|
||||
.eq("id", id)
|
||||
if (error) throw error
|
||||
},
|
||||
onSuccess: () =>
|
||||
queryClient.invalidateQueries({ queryKey: TEMPLATE_ITEMS_KEY }),
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Mutation: reorder items (accepts ordered array of ids)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
const reorderItems = useMutation({
|
||||
mutationFn: async (orderedIds: string[]) => {
|
||||
// Build an array of patch promises — one per item.
|
||||
const patches = orderedIds.map((id, index) =>
|
||||
supabase
|
||||
.from("template_items")
|
||||
.update({ sort_order: index })
|
||||
.eq("id", id)
|
||||
)
|
||||
const results = await Promise.all(patches)
|
||||
const firstError = results.find((r) => r.error)?.error
|
||||
if (firstError) throw firstError
|
||||
},
|
||||
onSuccess: () =>
|
||||
queryClient.invalidateQueries({ queryKey: TEMPLATE_ITEMS_KEY }),
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Exposed API
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
/** The single template row for the current user. */
|
||||
template: templateQuery.data ?? null,
|
||||
/** Template items sorted by sort_order, each with joined category data. */
|
||||
items: itemsQuery.data ?? [],
|
||||
loading: templateQuery.isLoading || itemsQuery.isLoading,
|
||||
updateName,
|
||||
createItem,
|
||||
updateItem,
|
||||
deleteItem,
|
||||
reorderItems,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user