159 lines
4.5 KiB
TypeScript
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;
|
|
},
|
|
});
|