208 lines
6.7 KiB
TypeScript
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,
|
|
}
|
|
}
|