Files
finanzen/convex/categories.ts
2026-06-15 11:33:23 +02:00

159 lines
4.5 KiB
TypeScript

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<string, unknown> = {};
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;
},
});