Files
SimpleFinanceDash/src/hooks/useTemplate.ts
2026-04-02 14:29:36 +02:00

208 lines
6.7 KiB
TypeScript

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,
}
}