Refactor financial transaction handling: Consolidate Einnahmen and Ausgaben into Buchung model, update routes and UI components, and add new migration scripts for database schema changes.
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
import { getApiUser } from "@/session.server";
|
||||
import prisma from "@/lib/prisma.server";
|
||||
import { z } from "zod";
|
||||
import { AusgabeKategorie } from "@prisma/client";
|
||||
|
||||
const updateSchema = z.object({
|
||||
kategorie: z.nativeEnum(AusgabeKategorie),
|
||||
kategorie: z.string().min(1),
|
||||
betrag: z.number().positive(),
|
||||
steuersatz: z.number().min(0).default(0),
|
||||
zahlungsart: z.enum(["KASSE", "BANK"]).default("BANK"),
|
||||
@@ -16,13 +15,13 @@ export async function action({ request, params }: { request: Request; params: {
|
||||
const user = await getApiUser(request);
|
||||
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
const ausgabe = await prisma.betriebsausgabe.findFirst({
|
||||
where: { id: params.id, company: { userId: user.id } },
|
||||
const buchung = await prisma.buchung.findFirst({
|
||||
where: { id: params.id, company: { userId: user.id }, type: "ENTNAHME", isBusinessRecord: true },
|
||||
});
|
||||
if (!ausgabe) return Response.json({ error: "Not found" }, { status: 404 });
|
||||
if (!buchung) return Response.json({ error: "Not found" }, { status: 404 });
|
||||
|
||||
if (request.method === "DELETE") {
|
||||
await prisma.betriebsausgabe.delete({ where: { id: params.id } });
|
||||
await prisma.buchung.delete({ where: { id: params.id } });
|
||||
return Response.json({ ok: true });
|
||||
}
|
||||
|
||||
@@ -30,22 +29,22 @@ export async function action({ request, params }: { request: Request; params: {
|
||||
const parsed = updateSchema.safeParse(body);
|
||||
if (!parsed.success) return Response.json({ error: parsed.error.issues }, { status: 400 });
|
||||
|
||||
const updated = await prisma.betriebsausgabe.update({
|
||||
const updated = await prisma.buchung.update({
|
||||
where: { id: params.id },
|
||||
data: {
|
||||
kategorie: parsed.data.kategorie,
|
||||
betrag: parsed.data.betrag,
|
||||
amount: parsed.data.betrag,
|
||||
steuersatz: parsed.data.steuersatz,
|
||||
zahlungsart: parsed.data.zahlungsart,
|
||||
datum: new Date(parsed.data.datum),
|
||||
beschreibung: parsed.data.beschreibung,
|
||||
account: parsed.data.zahlungsart === "KASSE" ? "KASSE" : "BANK",
|
||||
date: new Date(parsed.data.datum),
|
||||
description: parsed.data.beschreibung,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.json({
|
||||
...updated,
|
||||
betrag: Number(updated.betrag),
|
||||
steuersatz: Number(updated.steuersatz),
|
||||
datum: updated.datum.toISOString(),
|
||||
amount: Number(updated.amount),
|
||||
date: updated.date.toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
+17
-15
@@ -1,11 +1,10 @@
|
||||
import { getApiUser } from "@/session.server";
|
||||
import prisma from "@/lib/prisma.server";
|
||||
import { z } from "zod";
|
||||
import { AusgabeKategorie } from "@prisma/client";
|
||||
|
||||
const createSchema = z.object({
|
||||
companyId: z.string().min(1),
|
||||
kategorie: z.nativeEnum(AusgabeKategorie),
|
||||
kategorie: z.string().min(1),
|
||||
betrag: z.number().positive(),
|
||||
steuersatz: z.number().min(0).default(0),
|
||||
zahlungsart: z.enum(["KASSE", "BANK"]).default("BANK"),
|
||||
@@ -26,25 +25,26 @@ export async function loader({ request }: { request: Request }) {
|
||||
const company = await prisma.company.findFirst({ where: { id: companyId, userId: user.id } });
|
||||
if (!company) return Response.json({ error: "Not found" }, { status: 404 });
|
||||
|
||||
const ausgaben = await prisma.betriebsausgabe.findMany({
|
||||
const ausgaben = await prisma.buchung.findMany({
|
||||
where: {
|
||||
companyId,
|
||||
type: "ENTNAHME",
|
||||
isBusinessRecord: true,
|
||||
...(year ? {
|
||||
datum: {
|
||||
date: {
|
||||
gte: new Date(`${year}-01-01`),
|
||||
lt: new Date(`${year + 1}-01-01`),
|
||||
},
|
||||
} : {}),
|
||||
},
|
||||
orderBy: { datum: "desc" },
|
||||
orderBy: { date: "desc" },
|
||||
});
|
||||
|
||||
return Response.json(
|
||||
ausgaben.map((a) => ({
|
||||
...a,
|
||||
betrag: Number(a.betrag),
|
||||
steuersatz: Number(a.steuersatz),
|
||||
datum: a.datum.toISOString(),
|
||||
amount: Number(a.amount),
|
||||
date: a.date.toISOString(),
|
||||
}))
|
||||
);
|
||||
}
|
||||
@@ -62,22 +62,24 @@ export async function action({ request }: { request: Request }) {
|
||||
});
|
||||
if (!company) return Response.json({ error: "Company not found" }, { status: 404 });
|
||||
|
||||
const ausgabe = await prisma.betriebsausgabe.create({
|
||||
const ausgabe = await prisma.buchung.create({
|
||||
data: {
|
||||
companyId: parsed.data.companyId,
|
||||
account: parsed.data.zahlungsart === "KASSE" ? "KASSE" : "BANK",
|
||||
type: "ENTNAHME",
|
||||
amount: parsed.data.betrag,
|
||||
date: new Date(parsed.data.datum),
|
||||
description: parsed.data.beschreibung,
|
||||
kategorie: parsed.data.kategorie,
|
||||
betrag: parsed.data.betrag,
|
||||
steuersatz: parsed.data.steuersatz,
|
||||
zahlungsart: parsed.data.zahlungsart,
|
||||
datum: new Date(parsed.data.datum),
|
||||
beschreibung: parsed.data.beschreibung,
|
||||
isBusinessRecord: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.json({
|
||||
...ausgabe,
|
||||
betrag: Number(ausgabe.betrag),
|
||||
steuersatz: Number(ausgabe.steuersatz),
|
||||
datum: ausgabe.datum.toISOString(),
|
||||
amount: Number(ausgabe.amount),
|
||||
date: ausgabe.date.toISOString(),
|
||||
}, { status: 201 });
|
||||
}
|
||||
|
||||
+18
-18
@@ -63,42 +63,42 @@ export async function loader({ request }: { request: Request }) {
|
||||
const bank = Number(bankAgg._sum.grossTotal ?? 0);
|
||||
const summeAktiva = forderungen + bank;
|
||||
|
||||
// Betriebsausgaben für das Jahr
|
||||
const ausgaben = await prisma.betriebsausgabe.findMany({
|
||||
where: { companyId, datum: { gte: yearStart, lt: yearEnd } },
|
||||
// Betriebsausgaben für das Jahr (from buchungen with type=ENTNAHME and isBusinessRecord=true)
|
||||
const ausgaben = await prisma.buchung.findMany({
|
||||
where: { companyId, type: "ENTNAHME", isBusinessRecord: true, date: { gte: yearStart, lt: yearEnd } },
|
||||
});
|
||||
|
||||
const ausgabenGesamt = ausgaben.reduce((s, a) => s + Number(a.betrag), 0);
|
||||
const ausgabenGesamt = ausgaben.reduce((s, a) => s + Number(a.amount), 0);
|
||||
const ausgabenVorsteuer = ausgaben.reduce((s, a) => {
|
||||
const brutto = Number(a.betrag);
|
||||
const rate = Number(a.steuersatz) / 100;
|
||||
const brutto = Number(a.amount);
|
||||
const rate = (a.steuersatz || 0) / 100;
|
||||
return s + (rate > 0 ? Math.round((brutto / (1 + rate)) * rate * 100) / 100 : 0);
|
||||
}, 0);
|
||||
|
||||
// Ausgaben nach Kategorie
|
||||
const ausgabenByKategorieMap: Record<string, number> = {};
|
||||
for (const a of ausgaben) {
|
||||
const k = a.kategorie;
|
||||
ausgabenByKategorieMap[k] = (ausgabenByKategorieMap[k] ?? 0) + Number(a.betrag);
|
||||
const k = a.kategorie || "Sonstige";
|
||||
ausgabenByKategorieMap[k] = (ausgabenByKategorieMap[k] ?? 0) + Number(a.amount);
|
||||
}
|
||||
const ausgabenByKategorie = Object.entries(ausgabenByKategorieMap).map(([kategorie, betrag]) => ({ kategorie, betrag }));
|
||||
|
||||
// Sonstige Einnahmen für das Jahr
|
||||
const einnahmen = await prisma.betriebseinnahme.findMany({
|
||||
where: { companyId, datum: { gte: yearStart, lt: yearEnd } },
|
||||
// Sonstige Einnahmen für das Jahr (from buchungen with type=EINLAGE and isBusinessRecord=true)
|
||||
const einnahmen = await prisma.buchung.findMany({
|
||||
where: { companyId, type: "EINLAGE", isBusinessRecord: true, date: { gte: yearStart, lt: yearEnd } },
|
||||
});
|
||||
const sonstigeEinnahmen = einnahmen.reduce((s, e) => s + Number(e.betrag), 0);
|
||||
const sonstigeEinnahmen = einnahmen.reduce((s, e) => s + Number(e.amount), 0);
|
||||
const einnahmenUst = einnahmen.reduce((s, e) => {
|
||||
const brutto = Number(e.betrag);
|
||||
const rate = Number(e.steuersatz) / 100;
|
||||
const brutto = Number(e.amount);
|
||||
const rate = (e.steuersatz || 0) / 100;
|
||||
return s + (rate > 0 ? Math.round((brutto / (1 + rate)) * rate * 100) / 100 : 0);
|
||||
}, 0);
|
||||
|
||||
// Kasse / Bank aus sonstigen Einnahmen und Ausgaben (nach Zahlungsart)
|
||||
const einnahmenKasse = einnahmen.filter((e) => e.zahlungsart === "KASSE").reduce((s, e) => s + Number(e.betrag), 0);
|
||||
const ausgabenKasse = ausgaben.filter((a) => a.zahlungsart === "KASSE").reduce((s, a) => s + Number(a.betrag), 0);
|
||||
const einnahmenBank = einnahmen.filter((e) => e.zahlungsart === "BANK").reduce((s, e) => s + Number(e.betrag), 0);
|
||||
const ausgabenBank = ausgaben.filter((a) => a.zahlungsart === "BANK").reduce((s, a) => s + Number(a.betrag), 0);
|
||||
const einnahmenKasse = einnahmen.filter((e) => e.zahlungsart === "KASSE").reduce((s, e) => s + Number(e.amount), 0);
|
||||
const ausgabenKasse = ausgaben.filter((a) => a.zahlungsart === "KASSE").reduce((s, a) => s + Number(a.amount), 0);
|
||||
const einnahmenBank = einnahmen.filter((e) => e.zahlungsart === "BANK").reduce((s, e) => s + Number(e.amount), 0);
|
||||
const ausgabenBank = ausgaben.filter((a) => a.zahlungsart === "BANK").reduce((s, a) => s + Number(a.amount), 0);
|
||||
|
||||
// Kasse-Saldo = bezahlte Rechnungen (Kasse-Anteil wird nicht getrennt) + sonstige Einnahmen Kasse - Ausgaben Kasse
|
||||
// Bank-Näherung = bezahlte Rechnungen + sonstige Einnahmen Bank - Ausgaben Bank
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { getApiUser } from "@/session.server";
|
||||
import prisma from "@/lib/prisma.server";
|
||||
import { z } from "zod";
|
||||
|
||||
const createSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
typ: z.enum(["EINNAHME", "AUSGABE"]),
|
||||
});
|
||||
|
||||
/**
|
||||
* Loader: GET all categories for a company
|
||||
*
|
||||
* Query params:
|
||||
* - typ (optional): "EINNAHME" or "AUSGABE" to filter by type
|
||||
*
|
||||
* Returns a JSON array of BuchungKategorie records.
|
||||
*/
|
||||
export async function loader({ request, params }: { request: Request; params: { id: string } }) {
|
||||
const user = await getApiUser(request);
|
||||
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
const company = await prisma.company.findFirst({
|
||||
where: { id: params.id, userId: user.id },
|
||||
});
|
||||
if (!company) return Response.json({ error: "Company not found" }, { status: 404 });
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const typ = searchParams.get("typ");
|
||||
|
||||
const kategorien = await prisma.buchungKategorie.findMany({
|
||||
where: {
|
||||
companyId: params.id,
|
||||
...(typ ? { typ } : {}),
|
||||
},
|
||||
orderBy: { name: "asc" },
|
||||
});
|
||||
|
||||
return Response.json(kategorien);
|
||||
}
|
||||
|
||||
/**
|
||||
* Action: POST to create a new category, DELETE to remove one
|
||||
*
|
||||
* POST body:
|
||||
* { name: string, typ: "EINNAHME" | "AUSGABE" }
|
||||
*
|
||||
* DELETE query param:
|
||||
* - kategorieId: the id of the category to delete
|
||||
*/
|
||||
export async function action({ request, params }: { request: Request; params: { id: string } }) {
|
||||
const user = await getApiUser(request);
|
||||
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
const company = await prisma.company.findFirst({
|
||||
where: { id: params.id, userId: user.id },
|
||||
});
|
||||
if (!company) return Response.json({ error: "Company not found" }, { status: 404 });
|
||||
|
||||
if (request.method === "DELETE") {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const kategorieId = searchParams.get("kategorieId");
|
||||
|
||||
if (!kategorieId) {
|
||||
return Response.json({ error: "kategorieId required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check that kategorie belongs to this company
|
||||
const kategorie = await prisma.buchungKategorie.findFirst({
|
||||
where: { id: kategorieId, companyId: params.id },
|
||||
});
|
||||
if (!kategorie) return Response.json({ error: "Category not found" }, { status: 404 });
|
||||
|
||||
// Check if kategorie is in use
|
||||
const inUse = await prisma.buchung.findFirst({
|
||||
where: { kategorie: kategorie.name, companyId: params.id },
|
||||
});
|
||||
if (inUse) {
|
||||
return Response.json(
|
||||
{ error: "Category is in use and cannot be deleted" },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
await prisma.buchungKategorie.delete({ where: { id: kategorieId } });
|
||||
return Response.json({ ok: true });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const parsed = createSchema.safeParse(body);
|
||||
if (!parsed.success) return Response.json({ error: parsed.error.issues }, { status: 400 });
|
||||
|
||||
const kategorie = await prisma.buchungKategorie.create({
|
||||
data: {
|
||||
companyId: params.id,
|
||||
name: parsed.data.name,
|
||||
typ: parsed.data.typ,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.json(kategorie, { status: 201 });
|
||||
}
|
||||
@@ -8,9 +8,11 @@ type Transaction = {
|
||||
type: "einlage" | "entnahme";
|
||||
amount: number;
|
||||
description: string;
|
||||
isBusinessRecord: boolean;
|
||||
kategorie: string | null;
|
||||
};
|
||||
|
||||
function toTransaction(buchung: { id: string; date: Date; account: "KASSE" | "BANK"; type: "EINLAGE" | "ENTNAHME"; amount: { toString(): string } | number | string; description: string | null | undefined; }): Transaction {
|
||||
function toTransaction(buchung: { id: string; date: Date; account: "KASSE" | "BANK"; type: "EINLAGE" | "ENTNAHME"; amount: { toString(): string } | number | string; description: string | null | undefined; isBusinessRecord: boolean; kategorie: string | null | undefined }): Transaction {
|
||||
return {
|
||||
id: buchung.id,
|
||||
date: buchung.date.toISOString().split("T")[0],
|
||||
@@ -18,6 +20,8 @@ function toTransaction(buchung: { id: string; date: Date; account: "KASSE" | "BA
|
||||
type: buchung.type === "EINLAGE" ? "einlage" : "entnahme",
|
||||
amount: Number(buchung.amount),
|
||||
description: buchung.description || "",
|
||||
isBusinessRecord: buchung.isBusinessRecord,
|
||||
kategorie: buchung.kategorie || null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,10 +36,21 @@ export async function loader({ request, params }: { request: Request; params: {
|
||||
|
||||
if (!company) return Response.json({ error: "Company not found" }, { status: 404 });
|
||||
|
||||
const buchungen = await prisma.buchung.findMany({
|
||||
const buchungen = (await prisma.buchung.findMany({
|
||||
where: { companyId: id },
|
||||
orderBy: { date: "desc" },
|
||||
});
|
||||
select: {
|
||||
id: true,
|
||||
date: true,
|
||||
account: true,
|
||||
type: true,
|
||||
amount: true,
|
||||
description: true,
|
||||
isBusinessRecord: true,
|
||||
kategorie: true,
|
||||
},
|
||||
})) as unknown as Array<{ id: string; date: Date; account: "KASSE" | "BANK"; type: "EINLAGE" | "ENTNAHME"; amount: any; description: string | null; isBusinessRecord: boolean; kategorie: string | null }>;
|
||||
|
||||
|
||||
const transactions = buchungen.map(toTransaction);
|
||||
const balance = transactions.reduce((sum: number, t: Transaction) => sum + (t.type === "einlage" ? t.amount : -t.amount), 0 as number);
|
||||
@@ -60,10 +75,47 @@ export async function action({ request, params }: { request: Request; params: {
|
||||
|
||||
if (method === "POST") {
|
||||
const amount = Number(data.amount);
|
||||
if (!data.date || !data.account || !data.type || Number.isNaN(amount) || amount <= 0) {
|
||||
if (!data.date || !data.account || Number.isNaN(amount) || amount <= 0) {
|
||||
return Response.json({ error: "Ungültige Daten" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if this is an Umbuchung (transfer between accounts)
|
||||
if (data.type === "umbuchung") {
|
||||
if (!data.toAccount) {
|
||||
return Response.json({ error: "toAccount erforderlich für Umbuchung" }, { status: 400 });
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// ENTNAHME from source account
|
||||
const entnahme = await tx.buchung.create({
|
||||
data: {
|
||||
companyId: id,
|
||||
date: new Date(data.date),
|
||||
account: data.account === "bank" ? "BANK" : "KASSE",
|
||||
type: "ENTNAHME",
|
||||
amount: amount,
|
||||
description: data.description || "",
|
||||
},
|
||||
});
|
||||
|
||||
// EINLAGE to target account, linked to the entnahme
|
||||
await tx.buchung.create({
|
||||
data: {
|
||||
companyId: id,
|
||||
date: new Date(data.date),
|
||||
account: data.toAccount === "bank" ? "BANK" : "KASSE",
|
||||
type: "EINLAGE",
|
||||
amount: amount,
|
||||
description: data.description || "",
|
||||
linkedBuchungId: entnahme.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
} else {
|
||||
if (!data.type) {
|
||||
return Response.json({ error: "type erforderlich" }, { status: 400 });
|
||||
}
|
||||
|
||||
await prisma.buchung.create({
|
||||
data: {
|
||||
companyId: id,
|
||||
@@ -74,6 +126,7 @@ export async function action({ request, params }: { request: Request; params: {
|
||||
description: data.description || "",
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (method === "PUT") {
|
||||
if (!transactionId) return Response.json({ error: "transactionId required" }, { status: 400 });
|
||||
const amount = Number(data.amount);
|
||||
@@ -81,9 +134,28 @@ export async function action({ request, params }: { request: Request; params: {
|
||||
return Response.json({ error: "Ungültige Daten" }, { status: 400 });
|
||||
}
|
||||
|
||||
const exist = await prisma.buchung.findFirst({ where: { id: transactionId, companyId: id } });
|
||||
const exist = (await prisma.buchung.findFirst({
|
||||
where: { id: transactionId, companyId: id },
|
||||
select: {
|
||||
id: true,
|
||||
date: true,
|
||||
account: true,
|
||||
type: true,
|
||||
amount: true,
|
||||
description: true,
|
||||
isBusinessRecord: true,
|
||||
},
|
||||
})) as unknown as { id: string; date: Date; account: "KASSE" | "BANK"; type: "EINLAGE" | "ENTNAHME"; amount: any; description: string | null; isBusinessRecord: boolean } | null;
|
||||
if (!exist) return Response.json({ error: "Transaction not found" }, { status: 404 });
|
||||
|
||||
// Block edit if this is an auto-created Buchung (from Einnahme/Ausgabe)
|
||||
if (exist.isBusinessRecord) {
|
||||
return Response.json(
|
||||
{ error: "Automatisch erstellte Transaktionen können nicht direkt bearbeitet werden" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
await prisma.buchung.update({
|
||||
where: { id: transactionId },
|
||||
data: {
|
||||
@@ -97,12 +169,50 @@ export async function action({ request, params }: { request: Request; params: {
|
||||
} else if (method === "DELETE") {
|
||||
if (!transactionId) return Response.json({ error: "transactionId required" }, { status: 400 });
|
||||
|
||||
const exist = await prisma.buchung.findFirst({ where: { id: transactionId, companyId: id } });
|
||||
if (!exist) return Response.json({ error: "Transaction not found" }, { status: 404 });
|
||||
|
||||
// For Umbuchung (linked transactions), delete both
|
||||
const linkedId = (exist as any).linkedBuchungId;
|
||||
const isLinkedFrom = await prisma.buchung.findFirst({
|
||||
where: { linkedBuchungId: transactionId } as any,
|
||||
});
|
||||
|
||||
if (linkedId || isLinkedFrom) {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// If this is the ENTNAHME, delete linked EINLAGE
|
||||
if (linkedId) {
|
||||
await tx.buchung.deleteMany({ where: { id: linkedId } });
|
||||
}
|
||||
// If this is the EINLAGE, delete linked ENTNAHME
|
||||
if (isLinkedFrom) {
|
||||
await tx.buchung.deleteMany({ where: { id: isLinkedFrom.id } });
|
||||
}
|
||||
// Delete this entry
|
||||
await tx.buchung.deleteMany({ where: { id: transactionId, companyId: id } });
|
||||
});
|
||||
} else {
|
||||
// Regular transaction
|
||||
await prisma.buchung.deleteMany({ where: { id: transactionId, companyId: id } });
|
||||
}
|
||||
} else {
|
||||
return Response.json({ error: "Method not allowed" }, { status: 405 });
|
||||
}
|
||||
|
||||
const buchungen = await prisma.buchung.findMany({ where: { companyId: id }, orderBy: { date: "desc" } });
|
||||
const buchungen = await prisma.buchung.findMany({
|
||||
where: { companyId: id },
|
||||
orderBy: { date: "desc" },
|
||||
select: {
|
||||
id: true,
|
||||
date: true,
|
||||
account: true,
|
||||
type: true,
|
||||
amount: true,
|
||||
description: true,
|
||||
isBusinessRecord: true,
|
||||
kategorie: true,
|
||||
},
|
||||
});
|
||||
const transactions = buchungen.map(toTransaction);
|
||||
const balance = transactions.reduce((sum: number, t: Transaction) => sum + (t.type === "einlage" ? t.amount : -t.amount), 0 as number);
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { getApiUser } from "@/session.server";
|
||||
import prisma from "@/lib/prisma.server";
|
||||
import { z } from "zod";
|
||||
import { EinnahmeKategorie } from "@prisma/client";
|
||||
|
||||
const updateSchema = z.object({
|
||||
kategorie: z.nativeEnum(EinnahmeKategorie),
|
||||
kategorie: z.string().min(1),
|
||||
betrag: z.number().positive(),
|
||||
steuersatz: z.number().min(0).default(0),
|
||||
zahlungsart: z.enum(["KASSE", "BANK"]).default("BANK"),
|
||||
@@ -13,29 +12,25 @@ const updateSchema = z.object({
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles an API request to create, update or delete a einnahme.
|
||||
* Handles an API request to update or delete a einnahme (Buchung).
|
||||
*
|
||||
* @param {Request} request - The request object.
|
||||
* @param {Object} params - The route parameters.
|
||||
* @param {string} params.id - The id of the einnahme to update or delete.
|
||||
* @param {string} params.id - The id of the Buchung (einnahme) to update or delete.
|
||||
*
|
||||
* @returns {Promise<Response>} - A promise resolving to a Response object.
|
||||
*
|
||||
* @throws {Response} - If the request is unauthorized, returns a 401 response with an error message.
|
||||
* @throws {Response} - If the einnahme is not found, returns a 404 response with an error message.
|
||||
* @throws {Response} - If the request body is invalid, returns a 400 response with an error message containing the validation errors.
|
||||
*/
|
||||
export async function action({ request, params }: { request: Request; params: { id: string } }) {
|
||||
const user = await getApiUser(request);
|
||||
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
const einnahme = await prisma.betriebseinnahme.findFirst({
|
||||
where: { id: params.id, company: { userId: user.id } },
|
||||
const buchung = await prisma.buchung.findFirst({
|
||||
where: { id: params.id, company: { userId: user.id }, type: "EINLAGE", isBusinessRecord: true },
|
||||
});
|
||||
if (!einnahme) return Response.json({ error: "Not found" }, { status: 404 });
|
||||
if (!buchung) return Response.json({ error: "Not found" }, { status: 404 });
|
||||
|
||||
if (request.method === "DELETE") {
|
||||
await prisma.betriebseinnahme.delete({ where: { id: params.id } });
|
||||
await prisma.buchung.delete({ where: { id: params.id } });
|
||||
return Response.json({ ok: true });
|
||||
}
|
||||
|
||||
@@ -43,22 +38,22 @@ export async function action({ request, params }: { request: Request; params: {
|
||||
const parsed = updateSchema.safeParse(body);
|
||||
if (!parsed.success) return Response.json({ error: parsed.error.issues }, { status: 400 });
|
||||
|
||||
const updated = await prisma.betriebseinnahme.update({
|
||||
const updated = await prisma.buchung.update({
|
||||
where: { id: params.id },
|
||||
data: {
|
||||
kategorie: parsed.data.kategorie,
|
||||
betrag: parsed.data.betrag,
|
||||
amount: parsed.data.betrag,
|
||||
steuersatz: parsed.data.steuersatz,
|
||||
zahlungsart: parsed.data.zahlungsart,
|
||||
datum: new Date(parsed.data.datum),
|
||||
beschreibung: parsed.data.beschreibung,
|
||||
account: parsed.data.zahlungsart === "KASSE" ? "KASSE" : "BANK",
|
||||
date: new Date(parsed.data.datum),
|
||||
description: parsed.data.beschreibung,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.json({
|
||||
...updated,
|
||||
betrag: Number(updated.betrag),
|
||||
steuersatz: Number(updated.steuersatz),
|
||||
datum: updated.datum.toISOString(),
|
||||
amount: Number(updated.amount),
|
||||
date: updated.date.toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
+23
-19
@@ -1,11 +1,10 @@
|
||||
import { getApiUser } from "@/session.server";
|
||||
import prisma from "@/lib/prisma.server";
|
||||
import { z } from "zod";
|
||||
import { EinnahmeKategorie } from "@prisma/client";
|
||||
|
||||
const createSchema = z.object({
|
||||
companyId: z.string().min(1),
|
||||
kategorie: z.nativeEnum(EinnahmeKategorie),
|
||||
kategorie: z.string().min(1),
|
||||
betrag: z.number().positive(),
|
||||
steuersatz: z.number().min(0).default(0),
|
||||
zahlungsart: z.enum(["KASSE", "BANK"]).default("BANK"),
|
||||
@@ -18,7 +17,7 @@ const createSchema = z.object({
|
||||
*
|
||||
* Requires a companyId search parameter. If year is provided, filters einnahmen for the given year.
|
||||
*
|
||||
* Returns a list of einnahmen as a JSON object.
|
||||
* Returns a list of einnahmen (Buchungen with isBusinessRecord=true, type=EINLAGE) as a JSON object.
|
||||
*
|
||||
* If the request is unauthorized, returns a 401 response with an error message.
|
||||
* If the request body is invalid, returns a 400 response with an error message containing the validation errors.
|
||||
@@ -37,42 +36,45 @@ export async function loader({ request }: { request: Request }) {
|
||||
const company = await prisma.company.findFirst({ where: { id: companyId, userId: user.id } });
|
||||
if (!company) return Response.json({ error: "Not found" }, { status: 404 });
|
||||
|
||||
const einnahmen = await prisma.betriebseinnahme.findMany({
|
||||
const einnahmen = await prisma.buchung.findMany({
|
||||
where: {
|
||||
companyId,
|
||||
type: "EINLAGE",
|
||||
isBusinessRecord: true,
|
||||
...(year ? {
|
||||
datum: {
|
||||
date: {
|
||||
gte: new Date(`${year}-01-01`),
|
||||
lt: new Date(`${year + 1}-01-01`),
|
||||
},
|
||||
} : {}),
|
||||
},
|
||||
orderBy: { datum: "asc" },
|
||||
orderBy: { date: "asc" },
|
||||
});
|
||||
|
||||
return Response.json(
|
||||
einnahmen.map((e) => ({
|
||||
...e,
|
||||
betrag: Number(e.betrag),
|
||||
steuersatz: Number(e.steuersatz),
|
||||
datum: e.datum.toISOString(),
|
||||
amount: Number(e.amount),
|
||||
date: e.date.toISOString(),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new einnahme for a given company.
|
||||
* Creates a new einnahme (Buchung) for a given company.
|
||||
*
|
||||
* Requires a JSON object in the request body with the following shape:
|
||||
* {
|
||||
* companyId: string,
|
||||
* kategorie: EinnahmeKategorie,
|
||||
* kategorie: string (BuchungKategorie name),
|
||||
* betrag: number,
|
||||
* steuersatz: number,
|
||||
* zahlungsart: "KASSE" | "BANK",
|
||||
* datum: string,
|
||||
* beschreibung: string,
|
||||
* }
|
||||
*
|
||||
* Returns the created einnahme as a JSON object.
|
||||
* Returns the created Buchung as a JSON object.
|
||||
*
|
||||
* If the request is unauthorized, returns a 401 response with an error message.
|
||||
* If the request body is invalid, returns a 400 response with an error message containing the validation errors.
|
||||
@@ -91,24 +93,26 @@ export async function action({ request }: { request: Request }) {
|
||||
});
|
||||
if (!company) return Response.json({ error: "Company not found" }, { status: 404 });
|
||||
|
||||
const einnahme = await prisma.betriebseinnahme.create({
|
||||
const einnahme = await prisma.buchung.create({
|
||||
data: {
|
||||
companyId: parsed.data.companyId,
|
||||
account: parsed.data.zahlungsart === "KASSE" ? "KASSE" : "BANK",
|
||||
type: "EINLAGE",
|
||||
amount: parsed.data.betrag,
|
||||
date: new Date(parsed.data.datum),
|
||||
description: parsed.data.beschreibung,
|
||||
kategorie: parsed.data.kategorie,
|
||||
betrag: parsed.data.betrag,
|
||||
steuersatz: parsed.data.steuersatz,
|
||||
zahlungsart: parsed.data.zahlungsart,
|
||||
datum: new Date(parsed.data.datum),
|
||||
beschreibung: parsed.data.beschreibung,
|
||||
isBusinessRecord: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
...einnahme,
|
||||
betrag: Number(einnahme.betrag),
|
||||
steuersatz: Number(einnahme.steuersatz),
|
||||
datum: einnahme.datum.toISOString(),
|
||||
amount: Number(einnahme.amount),
|
||||
date: einnahme.date.toISOString(),
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
|
||||
@@ -103,6 +103,7 @@ export async function action({ request, params }: { request: Request; params: {
|
||||
if (!parsed.success) return Response.json({ error: parsed.error.issues }, { status: 400 });
|
||||
|
||||
const newStatus = parsed.data.status;
|
||||
const oldStatus = invoice.status;
|
||||
|
||||
let numberUpdate: string | null | undefined = undefined;
|
||||
if (newStatus === "DELETED") {
|
||||
@@ -111,10 +112,45 @@ export async function action({ request, params }: { request: Request; params: {
|
||||
numberUpdate = await generateInvoiceNumber(invoice.companyId);
|
||||
}
|
||||
|
||||
// Handle Buchung sync: Create when PAID, delete when unpaying
|
||||
if (newStatus === "PAID" && oldStatus !== "PAID") {
|
||||
// Create a Buchung for the invoice payment
|
||||
const buchung = await prisma.buchung.create({
|
||||
data: {
|
||||
companyId: invoice.companyId,
|
||||
date: invoice.issueDate,
|
||||
account: "BANK",
|
||||
type: "EINLAGE",
|
||||
amount: invoice.grossTotal,
|
||||
description: `Rechnung ${invoice.number}`,
|
||||
kategorie: "Rechnungseinnahme",
|
||||
isBusinessRecord: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Update invoice with buchungId
|
||||
const updated = await prisma.invoice.update({
|
||||
where: { id: params.id },
|
||||
data: {
|
||||
status: newStatus,
|
||||
buchungId: buchung.id,
|
||||
deletedAt: null,
|
||||
...(numberUpdate !== undefined && { number: numberUpdate }),
|
||||
},
|
||||
});
|
||||
return Response.json(updated);
|
||||
}
|
||||
|
||||
if (newStatus !== "PAID" && oldStatus === "PAID" && invoice.buchungId) {
|
||||
// Delete the linked Buchung when unpaying
|
||||
await prisma.buchung.delete({ where: { id: invoice.buchungId } });
|
||||
}
|
||||
|
||||
const updated = await prisma.invoice.update({
|
||||
where: { id: params.id },
|
||||
data: {
|
||||
status: newStatus,
|
||||
buchungId: newStatus === "PAID" ? invoice.buchungId : null,
|
||||
deletedAt: newStatus === "DELETED" ? new Date() : null,
|
||||
...(numberUpdate !== undefined && { number: numberUpdate }),
|
||||
},
|
||||
|
||||
@@ -52,12 +52,14 @@ export async function loader({ request, params }: { request: Request; params: {
|
||||
if (!company) throw new Response("Not Found", { status: 404 });
|
||||
|
||||
const year = new Date().getFullYear();
|
||||
const ausgaben = await prisma.betriebsausgabe.findMany({
|
||||
const ausgaben = await prisma.buchung.findMany({
|
||||
where: {
|
||||
companyId: params.id,
|
||||
datum: { gte: new Date(`${year}-01-01`), lt: new Date(`${year + 1}-01-01`) },
|
||||
type: "ENTNAHME",
|
||||
isBusinessRecord: true,
|
||||
date: { gte: new Date(`${year}-01-01`), lt: new Date(`${year + 1}-01-01`) },
|
||||
},
|
||||
orderBy: { datum: "desc" },
|
||||
orderBy: { date: "desc" },
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -66,12 +68,12 @@ export async function loader({ request, params }: { request: Request; params: {
|
||||
initialYear: year,
|
||||
ausgaben: ausgaben.map((a) => ({
|
||||
id: a.id,
|
||||
kategorie: a.kategorie as AusgabeKategorieKey,
|
||||
betrag: Number(a.betrag),
|
||||
steuersatz: Number(a.steuersatz),
|
||||
zahlungsart: a.zahlungsart as "KASSE" | "BANK",
|
||||
datum: a.datum.toISOString(),
|
||||
beschreibung: a.beschreibung,
|
||||
kategorie: (a.kategorie || "SONSTIGER_BETRIEBSBEDARF") as AusgabeKategorieKey,
|
||||
betrag: Number(a.amount),
|
||||
steuersatz: a.steuersatz || 19,
|
||||
zahlungsart: (a.zahlungsart as "KASSE" | "BANK") || "BANK",
|
||||
datum: a.date.toISOString(),
|
||||
beschreibung: a.description,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,12 +52,14 @@ export async function loader({ request, params }: { request: Request; params: {
|
||||
if (!company) throw new Response("Not Found", { status: 404 });
|
||||
|
||||
const year = new Date().getFullYear();
|
||||
const einnahmen = await prisma.betriebseinnahme.findMany({
|
||||
const einnahmen = await prisma.buchung.findMany({
|
||||
where: {
|
||||
companyId: params.id,
|
||||
datum: { gte: new Date(`${year}-01-01`), lt: new Date(`${year + 1}-01-01`) },
|
||||
type: "EINLAGE",
|
||||
isBusinessRecord: true,
|
||||
date: { gte: new Date(`${year}-01-01`), lt: new Date(`${year + 1}-01-01`) },
|
||||
},
|
||||
orderBy: { datum: "desc" },
|
||||
orderBy: { date: "desc" },
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -66,12 +68,12 @@ export async function loader({ request, params }: { request: Request; params: {
|
||||
initialYear: year,
|
||||
einnahmen: einnahmen.map((e) => ({
|
||||
id: e.id,
|
||||
kategorie: e.kategorie as EinnahmeKategorieKey,
|
||||
betrag: Number(e.betrag),
|
||||
steuersatz: Number(e.steuersatz),
|
||||
zahlungsart: e.zahlungsart as "KASSE" | "BANK",
|
||||
datum: e.datum.toISOString(),
|
||||
beschreibung: e.beschreibung,
|
||||
kategorie: (e.kategorie || "SONSTIGE_EINNAHMEN") as EinnahmeKategorieKey,
|
||||
betrag: Number(e.amount),
|
||||
steuersatz: e.steuersatz || 0,
|
||||
zahlungsart: (e.zahlungsart as "KASSE" | "BANK") || "BANK",
|
||||
datum: e.date.toISOString(),
|
||||
beschreibung: e.description,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ type Transaction = {
|
||||
type: 'einlage' | 'entnahme';
|
||||
amount: number;
|
||||
description: string;
|
||||
isBusinessRecord: boolean;
|
||||
kategorie: string | null;
|
||||
};
|
||||
|
||||
export async function loader({ request, params }: { request: Request; params: { id: string } }) {
|
||||
@@ -29,6 +31,16 @@ export async function loader({ request, params }: { request: Request; params: {
|
||||
const buchungen = await prisma.buchung.findMany({
|
||||
where: { companyId: company.id },
|
||||
orderBy: { date: 'desc' },
|
||||
select: {
|
||||
id: true,
|
||||
date: true,
|
||||
account: true,
|
||||
type: true,
|
||||
amount: true,
|
||||
description: true,
|
||||
isBusinessRecord: true,
|
||||
kategorie: true,
|
||||
},
|
||||
});
|
||||
|
||||
const transactions: Transaction[] = buchungen.map((b): Transaction => ({
|
||||
@@ -38,6 +50,8 @@ export async function loader({ request, params }: { request: Request; params: {
|
||||
type: b.type === 'EINLAGE' ? 'einlage' : 'entnahme',
|
||||
amount: Number(b.amount),
|
||||
description: b.description || '',
|
||||
isBusinessRecord: b.isBusinessRecord,
|
||||
kategorie: b.kategorie || null,
|
||||
}));
|
||||
|
||||
const balance = transactions.reduce((sum: number, t: Transaction) => sum + (t.type === 'einlage' ? t.amount : -t.amount), 0);
|
||||
@@ -59,22 +73,40 @@ export default function CompanyMoney() {
|
||||
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editingTransaction, setEditingTransaction] = useState<Transaction | null>(null);
|
||||
const [isUmbuchung, setIsUmbuchung] = useState(false);
|
||||
const [form, setForm] = useState({
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
account: 'kasse' as 'kasse' | 'bank',
|
||||
type: 'einlage' as 'einlage' | 'entnahme',
|
||||
amount: '',
|
||||
description: '',
|
||||
toAccount: 'bank' as 'kasse' | 'bank',
|
||||
});
|
||||
|
||||
function openCreate() {
|
||||
setEditingTransaction(null);
|
||||
setIsUmbuchung(false);
|
||||
setForm({
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
account: 'kasse',
|
||||
type: 'einlage',
|
||||
amount: '',
|
||||
description: '',
|
||||
toAccount: 'bank',
|
||||
});
|
||||
setDialogOpen(true);
|
||||
}
|
||||
|
||||
function openCreateUmbuchung() {
|
||||
setEditingTransaction(null);
|
||||
setIsUmbuchung(true);
|
||||
setForm({
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
account: 'kasse',
|
||||
type: 'umbuchung',
|
||||
amount: '',
|
||||
description: '',
|
||||
toAccount: 'bank',
|
||||
});
|
||||
setDialogOpen(true);
|
||||
}
|
||||
@@ -93,7 +125,16 @@ export default function CompanyMoney() {
|
||||
|
||||
async function handleSave() {
|
||||
setSaving(true);
|
||||
const payload = {
|
||||
const payload = isUmbuchung
|
||||
? {
|
||||
date: form.date,
|
||||
account: form.account,
|
||||
type: 'umbuchung',
|
||||
toAccount: form.toAccount,
|
||||
amount: parseFloat(form.amount),
|
||||
description: form.description,
|
||||
}
|
||||
: {
|
||||
date: form.date,
|
||||
account: form.account,
|
||||
type: form.type,
|
||||
@@ -103,17 +144,27 @@ export default function CompanyMoney() {
|
||||
|
||||
try {
|
||||
if (editingTransaction) {
|
||||
await fetch(`/api/companies/${companyId}/money?transactionId=${editingTransaction.id}`, {
|
||||
const res = await fetch(`/api/companies/${companyId}/money?transactionId=${editingTransaction.id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
alert(error.error || "Fehler beim Speichern");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
await fetch(`/api/companies/${companyId}/money`, {
|
||||
const res = await fetch(`/api/companies/${companyId}/money`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
alert(error.error || "Fehler beim Speichern");
|
||||
return;
|
||||
}
|
||||
}
|
||||
setDialogOpen(false);
|
||||
revalidate();
|
||||
@@ -158,6 +209,10 @@ export default function CompanyMoney() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button onClick={openCreateUmbuchung} variant="outline">
|
||||
<Plus className="h-4 w-4" />
|
||||
Umbuchung
|
||||
</Button>
|
||||
<Button onClick={openCreate}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Neue Transaktion
|
||||
@@ -187,7 +242,7 @@ export default function CompanyMoney() {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Tabelle */}
|
||||
{/* Split-View: Kasse und Bank nebeneinander */}
|
||||
{sortedTransactions.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="py-16 text-center text-gray-400">
|
||||
@@ -199,23 +254,28 @@ export default function CompanyMoney() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* Kasse Tabelle */}
|
||||
<Card>
|
||||
<div className="px-4 py-3 border-b border-slate-200 bg-slate-50">
|
||||
<h3 className="font-semibold text-slate-700">Kasse</h3>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200 bg-slate-50">
|
||||
<th className="px-4 py-2.5 text-left text-xs font-semibold text-slate-500 uppercase tracking-wide">
|
||||
<th className="px-3 py-2.5 text-left text-xs font-semibold text-slate-500 uppercase tracking-wide">
|
||||
Datum
|
||||
</th>
|
||||
<th className="px-3 py-2.5 text-left text-xs font-semibold text-slate-500 uppercase tracking-wide">
|
||||
Konto
|
||||
</th>
|
||||
<th className="px-4 py-2.5 text-left text-xs font-semibold text-slate-500 uppercase tracking-wide">
|
||||
Typ
|
||||
</th>
|
||||
<th className="px-3 py-2.5 text-left text-xs font-semibold text-slate-500 uppercase tracking-wide">
|
||||
Beschreibung
|
||||
</th>
|
||||
<th className="px-3 py-2.5 text-left text-xs font-semibold text-slate-500 uppercase tracking-wide">
|
||||
Kategorie
|
||||
</th>
|
||||
<th className="px-3 py-2.5 text-right text-xs font-semibold text-slate-500 uppercase tracking-wide">
|
||||
Betrag
|
||||
</th>
|
||||
@@ -223,15 +283,14 @@ export default function CompanyMoney() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{sortedTransactions.map((transaction) => (
|
||||
{sortedTransactions
|
||||
.filter((t) => t.account === 'kasse')
|
||||
.map((transaction) => (
|
||||
<tr key={transaction.id} className="hover:bg-slate-50/60 group">
|
||||
<td className="px-4 py-2.5 text-slate-600 whitespace-nowrap">
|
||||
<td className="px-3 py-2.5 text-slate-600 whitespace-nowrap">
|
||||
{transaction.date}
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-slate-600 whitespace-nowrap">
|
||||
{transaction.account === 'kasse' ? 'Kasse' : 'Bank'}
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-slate-600 whitespace-nowrap">
|
||||
<td className="px-3 py-2.5 text-slate-600 whitespace-nowrap">
|
||||
<Badge variant={transaction.type === 'einlage' ? 'default' : 'destructive'}>
|
||||
{transaction.type === 'einlage' ? 'Einlage' : 'Entnahme'}
|
||||
</Badge>
|
||||
@@ -239,12 +298,20 @@ export default function CompanyMoney() {
|
||||
<td className="px-3 py-2.5 text-slate-700">
|
||||
{transaction.description}
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-slate-600 text-xs">
|
||||
{transaction.kategorie || '—'}
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-right font-medium whitespace-nowrap">
|
||||
<span className={transaction.type === 'einlage' ? 'text-green-600' : 'text-red-600'}>
|
||||
{transaction.type === 'einlage' ? '+' : '-'}{formatCurrency(transaction.amount)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-2.5">
|
||||
{transaction.isBusinessRecord ? (
|
||||
<span className="text-xs text-gray-500 font-medium">
|
||||
Automatisch
|
||||
</span>
|
||||
) : (
|
||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={() => openEdit(transaction)}
|
||||
@@ -266,6 +333,7 @@ export default function CompanyMoney() {
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -273,6 +341,94 @@ export default function CompanyMoney() {
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Bank Tabelle */}
|
||||
<Card>
|
||||
<div className="px-4 py-3 border-b border-slate-200 bg-slate-50">
|
||||
<h3 className="font-semibold text-slate-700">Bank</h3>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200 bg-slate-50">
|
||||
<th className="px-3 py-2.5 text-left text-xs font-semibold text-slate-500 uppercase tracking-wide">
|
||||
Datum
|
||||
</th>
|
||||
<th className="px-3 py-2.5 text-left text-xs font-semibold text-slate-500 uppercase tracking-wide">
|
||||
Typ
|
||||
</th>
|
||||
<th className="px-3 py-2.5 text-left text-xs font-semibold text-slate-500 uppercase tracking-wide">
|
||||
Beschreibung
|
||||
</th>
|
||||
<th className="px-3 py-2.5 text-left text-xs font-semibold text-slate-500 uppercase tracking-wide">
|
||||
Kategorie
|
||||
</th>
|
||||
<th className="px-3 py-2.5 text-right text-xs font-semibold text-slate-500 uppercase tracking-wide">
|
||||
Betrag
|
||||
</th>
|
||||
<th className="px-3 py-2.5 w-16" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{sortedTransactions
|
||||
.filter((t) => t.account === 'bank')
|
||||
.map((transaction) => (
|
||||
<tr key={transaction.id} className="hover:bg-slate-50/60 group">
|
||||
<td className="px-3 py-2.5 text-slate-600 whitespace-nowrap">
|
||||
{transaction.date}
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-slate-600 whitespace-nowrap">
|
||||
<Badge variant={transaction.type === 'einlage' ? 'default' : 'destructive'}>
|
||||
{transaction.type === 'einlage' ? 'Einlage' : 'Entnahme'}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-slate-700">
|
||||
{transaction.description}
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-slate-600 text-xs">
|
||||
{transaction.kategorie || '—'}
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-right font-medium whitespace-nowrap">
|
||||
<span className={transaction.type === 'einlage' ? 'text-green-600' : 'text-red-600'}>
|
||||
{transaction.type === 'einlage' ? '+' : '-'}{formatCurrency(transaction.amount)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-2.5">
|
||||
{transaction.isBusinessRecord ? (
|
||||
<span className="text-xs text-gray-500 font-medium">
|
||||
Automatisch
|
||||
</span>
|
||||
) : (
|
||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={() => openEdit(transaction)}
|
||||
className="p-1 rounded hover:bg-slate-100 text-slate-500 hover:text-slate-700"
|
||||
title="Bearbeiten"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(transaction.id)}
|
||||
disabled={deleting === transaction.id}
|
||||
className="p-1 rounded hover:bg-red-50 text-slate-400 hover:text-red-600"
|
||||
title="Löschen"
|
||||
>
|
||||
{deleting === transaction.id ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dialog: Anlegen / Bearbeiten */}
|
||||
@@ -297,6 +453,32 @@ export default function CompanyMoney() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isUmbuchung ? (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Von (Konto) <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
value={form.account}
|
||||
onChange={(e) => setForm((f) => ({ ...f, account: e.target.value as 'kasse' | 'bank', toAccount: e.target.value === 'kasse' ? 'bank' : 'kasse' }))}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
>
|
||||
<option value="kasse">Kasse</option>
|
||||
<option value="bank">Bank</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Nach (Konto)
|
||||
</label>
|
||||
<div className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm bg-gray-50">
|
||||
{form.toAccount === 'kasse' ? 'Kasse' : 'Bank'}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Konto <span className="text-red-500">*</span>
|
||||
@@ -320,10 +502,12 @@ export default function CompanyMoney() {
|
||||
onChange={(e) => setForm((f) => ({ ...f, type: e.target.value as 'einlage' | 'entnahme' }))}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
>
|
||||
<option value="einlage">Einlage</option>
|
||||
<option value="entnahme">Entnahme</option>
|
||||
<option value="einlage">Einnahme (Einlage)</option>
|
||||
<option value="entnahme">Ausgabe (Entnahme)</option>
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
@@ -360,7 +544,7 @@ export default function CompanyMoney() {
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={saving || !formValid}>
|
||||
{saving && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
{editingTransaction ? "Speichern" : "Hinzufügen"}
|
||||
{editingTransaction ? "Speichern" : isUmbuchung ? "Umbuchung durchführen" : "Hinzufügen"}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
Generated
-1
@@ -1311,7 +1311,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz",
|
||||
"integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=16.13"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
-- Add source linking fields to Buchung model
|
||||
ALTER TABLE `buchungen` ADD COLUMN `betriebseinnahmeId` VARCHAR(191) NULL;
|
||||
ALTER TABLE `buchungen` ADD COLUMN `betriebsausgabeId` VARCHAR(191) NULL;
|
||||
ALTER TABLE `buchungen` ADD COLUMN `linkedBuchungId` VARCHAR(191) NULL;
|
||||
|
||||
-- Add unique constraints for 1:1 relations
|
||||
ALTER TABLE `buchungen` ADD UNIQUE INDEX `buchungen_betriebseinnahmeId_key`(`betriebseinnahmeId`);
|
||||
ALTER TABLE `buchungen` ADD UNIQUE INDEX `buchungen_betriebsausgabeId_key`(`betriebsausgabeId`);
|
||||
|
||||
-- Add foreign key constraints
|
||||
ALTER TABLE `buchungen` ADD CONSTRAINT `buchungen_betriebseinnahmeId_fkey` FOREIGN KEY (`betriebseinnahmeId`) REFERENCES `betriebseinnahmen`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE `buchungen` ADD CONSTRAINT `buchungen_betriebsausgabeId_fkey` FOREIGN KEY (`betriebsausgabeId`) REFERENCES `betriebsausgaben`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE `buchungen` ADD CONSTRAINT `buchungen_linkedBuchungId_fkey` FOREIGN KEY (`linkedBuchungId`) REFERENCES `buchungen`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- Add indexes for linked fields
|
||||
ALTER TABLE `buchungen` ADD INDEX `buchungen_betriebsausgabeId_idx`(`betriebsausgabeId`);
|
||||
ALTER TABLE `buchungen` ADD INDEX `buchungen_linkedBuchungId_idx`(`linkedBuchungId`);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Add buchungId field to invoices table
|
||||
ALTER TABLE `invoices` ADD COLUMN `buchungId` VARCHAR(191) NULL;
|
||||
|
||||
-- Create unique index for the foreign key
|
||||
ALTER TABLE `invoices` ADD UNIQUE INDEX `invoices_buchungId_key`(`buchungId`);
|
||||
|
||||
-- Add foreign key constraint
|
||||
ALTER TABLE `invoices` ADD CONSTRAINT `invoices_buchungId_fkey`
|
||||
FOREIGN KEY (`buchungId`) REFERENCES `buchungen`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,41 @@
|
||||
-- CreateTable for BuchungKategorie
|
||||
CREATE TABLE `buchung_kategorien` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`companyId` VARCHAR(191) NOT NULL,
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`typ` VARCHAR(191) NOT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
|
||||
INDEX `buchung_kategorien_companyId_idx`(`companyId`),
|
||||
UNIQUE INDEX `buchung_kategorien_companyId_name_typ_key`(`companyId`, `name`, `typ`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- AddForeignKey for BuchungKategorie
|
||||
ALTER TABLE `buchung_kategorien` ADD CONSTRAINT `buchung_kategorien_companyId_fkey` FOREIGN KEY (`companyId`) REFERENCES `companies`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AlterTable `buchungen` - add new columns
|
||||
ALTER TABLE `buchungen` ADD COLUMN `kategorie` VARCHAR(191) NULL;
|
||||
ALTER TABLE `buchungen` ADD COLUMN `steuersatz` INT NULL;
|
||||
ALTER TABLE `buchungen` ADD COLUMN `zahlungsart` ENUM('KASSE', 'BANK') NULL;
|
||||
ALTER TABLE `buchungen` ADD COLUMN `isBusinessRecord` BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- Add index for isBusinessRecord
|
||||
ALTER TABLE `buchungen` ADD INDEX `buchungen_isBusinessRecord_idx`(`isBusinessRecord`);
|
||||
|
||||
-- Migrate existing data from betriebseinnahmen/betriebsausgaben to Buchung
|
||||
-- This is handled by the post-migration script
|
||||
|
||||
-- AlterTable `betriebseinnahmen` - change kategorie from Enum to String
|
||||
ALTER TABLE `betriebseinnahmen` MODIFY `kategorie` VARCHAR(191) NOT NULL;
|
||||
ALTER TABLE `betriebseinnahmen` MODIFY `steuersatz` INT NOT NULL DEFAULT 0;
|
||||
|
||||
-- AlterTable `betriebsausgaben` - change kategorie from Enum to String
|
||||
ALTER TABLE `betriebsausgaben` MODIFY `kategorie` VARCHAR(191) NOT NULL;
|
||||
ALTER TABLE `betriebsausgaben` MODIFY `steuersatz` INT NOT NULL DEFAULT 0;
|
||||
|
||||
-- Drop old foreign key constraints from buchungen (if they exist from previous migration)
|
||||
-- These will be re-added if needed, but for now we're consolidating
|
||||
|
||||
-- Note: The enum columns EinnahmeKategorie and AusgabeKategorie are automatically
|
||||
-- dropped by Prisma when they're no longer referenced in the schema.
|
||||
@@ -0,0 +1,61 @@
|
||||
-- Migration: Drop legacy betriebseinnahmen and betriebsausgaben tables
|
||||
-- This consolidates all transaction data into the buchungen table
|
||||
|
||||
-- STEP 1: Copy existing betriebseinnahmen into buchungen
|
||||
-- Only copy those that don't already have a linked Buchung (via betriebseinnahmeId)
|
||||
INSERT INTO `buchungen` (id, companyId, date, account, type, amount, description,
|
||||
kategorie, steuersatz, zahlungsart, isBusinessRecord, createdAt, updatedAt)
|
||||
SELECT
|
||||
CONCAT('migr-ein-', e.id),
|
||||
e.companyId,
|
||||
e.datum,
|
||||
e.zahlungsart,
|
||||
'EINLAGE',
|
||||
e.betrag,
|
||||
e.beschreibung,
|
||||
e.kategorie,
|
||||
e.steuersatz,
|
||||
e.zahlungsart,
|
||||
true,
|
||||
e.createdAt,
|
||||
e.updatedAt
|
||||
FROM `betriebseinnahmen` e
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `buchungen` b WHERE b.betriebseinnahmeId = e.id
|
||||
);
|
||||
|
||||
-- STEP 2: Copy existing betriebsausgaben into buchungen
|
||||
INSERT INTO `buchungen` (id, companyId, date, account, type, amount, description,
|
||||
kategorie, steuersatz, zahlungsart, isBusinessRecord, createdAt, updatedAt)
|
||||
SELECT
|
||||
CONCAT('migr-aus-', a.id),
|
||||
a.companyId,
|
||||
a.datum,
|
||||
a.zahlungsart,
|
||||
'ENTNAHME',
|
||||
a.betrag,
|
||||
a.beschreibung,
|
||||
a.kategorie,
|
||||
a.steuersatz,
|
||||
a.zahlungsart,
|
||||
true,
|
||||
a.createdAt,
|
||||
a.updatedAt
|
||||
FROM `betriebsausgaben` a
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `buchungen` b WHERE b.betriebsausgabeId = a.id
|
||||
);
|
||||
|
||||
-- STEP 3: Remove FK constraints before dropping columns/tables
|
||||
ALTER TABLE `buchungen` DROP FOREIGN KEY `buchungen_betriebseinnahmeId_fkey`;
|
||||
ALTER TABLE `buchungen` DROP FOREIGN KEY `buchungen_betriebsausgabeId_fkey`;
|
||||
|
||||
-- STEP 4: Remove old linking columns from buchungen
|
||||
ALTER TABLE `buchungen` DROP INDEX `buchungen_betriebseinnahmeId_key`;
|
||||
ALTER TABLE `buchungen` DROP INDEX `buchungen_betriebsausgabeId_key`;
|
||||
ALTER TABLE `buchungen` DROP COLUMN `betriebseinnahmeId`;
|
||||
ALTER TABLE `buchungen` DROP COLUMN `betriebsausgabeId`;
|
||||
|
||||
-- STEP 5: Drop old tables
|
||||
DROP TABLE `betriebsausgaben`;
|
||||
DROP TABLE `betriebseinnahmen`;
|
||||
+25
-69
@@ -70,16 +70,28 @@ model Company {
|
||||
customers Customer[]
|
||||
invoices Invoice[]
|
||||
services Service[]
|
||||
betriebsausgaben Betriebsausgabe[]
|
||||
betriebseinnahmen Betriebseinnahme[]
|
||||
anlagegueter Anlagegut[]
|
||||
buchungen Buchung[]
|
||||
kategorien BuchungKategorie[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("companies")
|
||||
}
|
||||
|
||||
model BuchungKategorie {
|
||||
id String @id @default(cuid())
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
name String // e.g., "Fußpflege", "Miete", "Privateinlagen"
|
||||
typ String // "EINNAHME" or "AUSGABE"
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([companyId, name, typ])
|
||||
@@index([companyId])
|
||||
@@map("buchung_kategorien")
|
||||
}
|
||||
|
||||
enum TransactionAccount {
|
||||
KASSE
|
||||
BANK
|
||||
@@ -99,11 +111,20 @@ model Buchung {
|
||||
type TransactionType
|
||||
amount Decimal @db.Decimal(10, 2)
|
||||
description String? @db.Text
|
||||
kategorie String? // Name of BuchungKategorie (nullable for manual transactions)
|
||||
steuersatz Int? // Tax rate: 0, 7, 19 (nullable for non-business records)
|
||||
zahlungsart Zahlungsart? // KASSE or BANK
|
||||
isBusinessRecord Boolean @default(false) // True if this is from Einnahme/Ausgabe
|
||||
linkedBuchungId String?
|
||||
linkedBuchung Buchung? @relation("BuchungLink", fields: [linkedBuchungId], references: [id], onDelete: SetNull)
|
||||
linkedFrom Buchung[] @relation("BuchungLink")
|
||||
invoice Invoice? // Back-relation: Invoice -> Buchung (via buchungId)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([companyId])
|
||||
@@index([date])
|
||||
@@index([isBusinessRecord])
|
||||
@@map("buchungen")
|
||||
}
|
||||
|
||||
@@ -158,6 +179,8 @@ model Invoice {
|
||||
netTotal Decimal @db.Decimal(10, 2)
|
||||
taxTotal Decimal @db.Decimal(10, 2)
|
||||
grossTotal Decimal @db.Decimal(10, 2)
|
||||
buchungId String? @unique // Link to auto-created Buchung when PAID
|
||||
buchung Buchung? @relation(fields: [buchungId], references: [id], onDelete: SetNull)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
@@ -179,36 +202,6 @@ enum Zahlungsart {
|
||||
BANK
|
||||
}
|
||||
|
||||
enum EinnahmeKategorie {
|
||||
FUSSPFLEGE
|
||||
PRIVATEINLAGEN
|
||||
DARLEHEN
|
||||
STEUERERSTATTUNGEN
|
||||
VERSICHERUNGSERSTATTUNGEN
|
||||
ZINSERTRAEGE
|
||||
VERMIETUNG_VERPACHTUNG
|
||||
VERAEUSSERUNGSERLOES
|
||||
EIGENVERBRAUCH
|
||||
SONSTIGE_EINNAHMEN
|
||||
}
|
||||
|
||||
model Betriebseinnahme {
|
||||
id String @id @default(cuid())
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
kategorie EinnahmeKategorie
|
||||
betrag Decimal @db.Decimal(10, 2)
|
||||
steuersatz Decimal @db.Decimal(5, 2) @default(0)
|
||||
zahlungsart Zahlungsart @default(BANK)
|
||||
datum DateTime
|
||||
beschreibung String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([companyId])
|
||||
@@index([datum])
|
||||
@@map("betriebseinnahmen")
|
||||
}
|
||||
|
||||
model Anlagegut {
|
||||
id String @id @default(cuid())
|
||||
@@ -228,43 +221,6 @@ model Anlagegut {
|
||||
@@map("anlagegueter")
|
||||
}
|
||||
|
||||
enum AusgabeKategorie {
|
||||
WAREN_ROHSTOFFE
|
||||
GERINGWERTIGE_WIRTSCHAFTSGUETER
|
||||
ABSCHREIBUNGEN
|
||||
MIETE
|
||||
STROM_WASSER
|
||||
TELEKOMMUNIKATION
|
||||
FORTBILDUNG_MESSEN
|
||||
BEITRAEGE
|
||||
VERSICHERUNGEN
|
||||
WERBEKOSTEN
|
||||
ZINSEN
|
||||
REISEKOSTEN
|
||||
REPARATUREN_INSTANDHALTUNG
|
||||
BUEROBEDARF
|
||||
REPRAESENTATIONSKOSTEN
|
||||
SONSTIGER_BETRIEBSBEDARF
|
||||
NEBENKOSTEN_GELDVERKEHR
|
||||
}
|
||||
|
||||
model Betriebsausgabe {
|
||||
id String @id @default(cuid())
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
kategorie AusgabeKategorie
|
||||
betrag Decimal @db.Decimal(10, 2)
|
||||
steuersatz Decimal @db.Decimal(5, 2) @default(0)
|
||||
zahlungsart Zahlungsart @default(BANK)
|
||||
datum DateTime
|
||||
beschreibung String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([companyId])
|
||||
@@index([datum])
|
||||
@@map("betriebsausgaben")
|
||||
}
|
||||
|
||||
model InvoiceItem {
|
||||
id String @id @default(cuid())
|
||||
|
||||
+53
-1
@@ -136,8 +136,60 @@ async function main() {
|
||||
data: { invoiceSequence: 1 },
|
||||
});
|
||||
|
||||
// Seed BuchungKategorien for demo company
|
||||
const einnahmeKategorien = [
|
||||
"Fußpflege",
|
||||
"Privateinlagen",
|
||||
"Darlehen",
|
||||
"Steuererstattungen",
|
||||
"Versicherungserstattungen",
|
||||
"Zinsertrage",
|
||||
"Vermietung/Verpachtung",
|
||||
"Veräußerungserlös",
|
||||
"Eigenverbrauch",
|
||||
"Sonstige Einnahmen",
|
||||
];
|
||||
|
||||
const ausgabeKategorien = [
|
||||
"Waren und Rohstoffe",
|
||||
"Geringwertige Wirtschaftsgüter",
|
||||
"Abschreibungen",
|
||||
"Miete",
|
||||
"Strom und Wasser",
|
||||
"Telekommunikation",
|
||||
"Fortbildung und Messen",
|
||||
"Beiträge",
|
||||
"Versicherungen",
|
||||
"Werbekosten",
|
||||
"Zinsen",
|
||||
"Reisekosten",
|
||||
"Reparaturen und Instandhaltung",
|
||||
"Bürobedarf",
|
||||
"Repräsentationskosten",
|
||||
"Sonstiger Betriebsbedarf",
|
||||
"Nebenkosten Geldverkehr",
|
||||
];
|
||||
|
||||
for (const name of einnahmeKategorien) {
|
||||
await prisma.buchungKategorie.upsert({
|
||||
where: { companyId_name_typ: { companyId: company.id, name, typ: "EINNAHME" } },
|
||||
update: {},
|
||||
create: { companyId: company.id, name, typ: "EINNAHME" },
|
||||
});
|
||||
}
|
||||
console.log(`✓ Seeded ${einnahmeKategorien.length} Einnahme categories`);
|
||||
|
||||
for (const name of ausgabeKategorien) {
|
||||
await prisma.buchungKategorie.upsert({
|
||||
where: { companyId_name_typ: { companyId: company.id, name, typ: "AUSGABE" } },
|
||||
update: {},
|
||||
create: { companyId: company.id, name, typ: "AUSGABE" },
|
||||
});
|
||||
}
|
||||
console.log(`✓ Seeded ${ausgabeKategorien.length} Ausgabe categories`);
|
||||
|
||||
console.log("\n✅ Seed complete!");
|
||||
console.log("Login: anna@example.de / demo123");
|
||||
console.log("Login: anna@example.de / annas_password");
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user