import { query, mutation } from "./_generated/server"; import { v } from "convex/values"; import { assertOwned, requireUserId } from "./lib/helpers"; import { DEFAULT_CATEGORIES } from "./lib/seedCategories"; const categoryValidator = v.object({ _id: v.id("categories"), _creationTime: v.number(), userId: v.id("users"), name: v.string(), kind: v.union(v.literal("einnahme"), v.literal("ausgabe")), block: v.optional(v.union(v.literal("wiederkehrend"), v.literal("variabel"))), color: v.string(), icon: v.optional(v.string()), sortOrder: v.number(), isSystem: v.boolean(), }); export const list = query({ args: {}, returns: v.array(categoryValidator), handler: async (ctx) => { const userId = await requireUserId(ctx); const categories = await ctx.db .query("categories") .withIndex("by_user", (q) => q.eq("userId", userId)) .collect(); return categories.sort((a, b) => a.sortOrder - b.sortOrder); }, }); export const create = mutation({ args: { name: v.string(), kind: v.union(v.literal("einnahme"), v.literal("ausgabe")), block: v.optional(v.union(v.literal("wiederkehrend"), v.literal("variabel"))), color: v.string(), icon: v.optional(v.string()), sortOrder: v.number(), }, returns: v.id("categories"), handler: async (ctx, args) => { const userId = await requireUserId(ctx); if (args.kind === "ausgabe" && !args.block) { throw new Error("Block ist bei Ausgaben Pflicht"); } const existing = await ctx.db .query("categories") .withIndex("by_user_name", (q) => q.eq("userId", userId).eq("name", args.name)) .unique(); if (existing) throw new Error("Kategorie existiert bereits"); return await ctx.db.insert("categories", { userId, name: args.name, kind: args.kind, block: args.kind === "ausgabe" ? args.block : undefined, color: args.color, icon: args.icon, sortOrder: args.sortOrder, isSystem: false, }); }, }); export const update = mutation({ args: { id: v.id("categories"), name: v.optional(v.string()), kind: v.optional(v.union(v.literal("einnahme"), v.literal("ausgabe"))), block: v.optional(v.union(v.literal("wiederkehrend"), v.literal("variabel"))), color: v.optional(v.string()), icon: v.optional(v.string()), sortOrder: v.optional(v.number()), }, returns: v.null(), handler: async (ctx, args) => { const userId = await requireUserId(ctx); const category = await assertOwned( await ctx.db.get("categories", args.id), userId, "Kategorie", ); const { id, ...updates } = args; const patch: Record = {}; for (const [key, value] of Object.entries(updates)) { if (value !== undefined) patch[key] = value; } const kind = (patch.kind as typeof category.kind | undefined) ?? category.kind; const block = (patch.block as typeof category.block | undefined) ?? category.block; if (kind === "ausgabe" && !block) { throw new Error("Block ist bei Ausgaben Pflicht"); } if (kind === "einnahme") { patch.block = undefined; } if (Object.keys(patch).length > 0) { await ctx.db.patch(id, patch); } return null; }, }); export const remove = mutation({ args: { id: v.id("categories") }, returns: v.null(), handler: async (ctx, args) => { const userId = await requireUserId(ctx); const category = await assertOwned( await ctx.db.get("categories", args.id), userId, "Kategorie", ); if (category.isSystem) { throw new Error("System-Kategorien können nicht gelöscht werden"); } const txs = await ctx.db .query("transactions") .withIndex("by_user_category", (q) => q.eq("userId", userId).eq("categoryId", args.id), ) .collect(); for (const tx of txs) { await ctx.db.patch(tx._id, { categoryId: undefined }); } await ctx.db.delete(args.id); return null; }, }); export const seedDefaults = mutation({ args: {}, returns: v.null(), handler: async (ctx) => { const userId = await requireUserId(ctx); const existing = await ctx.db .query("categories") .withIndex("by_user", (q) => q.eq("userId", userId)) .first(); if (existing) return null; for (const cat of DEFAULT_CATEGORIES) { await ctx.db.insert("categories", { userId, name: cat.name, kind: cat.kind, block: cat.block, color: cat.color, icon: cat.icon, sortOrder: cat.sortOrder, isSystem: true, }); } return null; }, });