From d582c748a2f5d4398cfb7bcd43a7bfa792c41041 Mon Sep 17 00:00:00 2001 From: hwinkel Date: Tue, 24 Mar 2026 19:25:48 +0100 Subject: [PATCH] feat: add financial transactions management for companies - Implemented a new route for managing financial transactions (money) for companies, including creating, editing, and deleting transactions. - Added a new model `Buchung` to represent transactions with fields for date, account type, transaction type, amount, and description. - Updated the `companies` model to include a relation to the new `Buchung` model. - Enhanced the company overview page to link to the new financial transactions page. - Added migration scripts to create the necessary database tables and fields for the new functionality. - Created utility scripts for resetting the admin password and setting up the initial admin user. --- .claude/settings.json | 11 - .react-router/types/+routes.ts | 52 +- .../routes/+types/api.anlagevermoegen.$id.ts | 62 ++ .../app/routes/+types/api.anlagevermoegen.ts | 62 ++ .../routes/+types/api.companies.$id.money.ts | 62 ++ .../+types/companies.$id.anlagevermoegen.ts | 65 ++ .../app/routes/+types/companies.$id.money.ts | 65 ++ app/entry.server.tsx | 2 +- app/routes.ts | 6 + app/routes/api.anlagevermoegen.$id.ts | 52 ++ app/routes/api.anlagevermoegen.ts | 104 +++ app/routes/api.ausgaben.$id.ts | 11 +- app/routes/api.ausgaben.ts | 12 +- app/routes/api.bilanzen.ts | 61 +- app/routes/api.companies.$id.money.ts | 110 +++ app/routes/api.einnahmen.$id.ts | 11 +- app/routes/api.einnahmen.ts | 12 +- app/routes/companies.$id.anlagevermoegen.tsx | 698 +++++++++++------- app/routes/companies.$id.ausgaben.tsx | 581 +++++++++------ app/routes/companies.$id.bilanzen.tsx | 34 +- app/routes/companies.$id.einnahmen.tsx | 605 ++++++++------- app/routes/companies.$id.money.tsx | 370 ++++++++++ app/routes/companies.$id.tsx | 22 +- .../migration.sql | 7 + .../migration.sql | 2 + .../migration.sql | 28 + prisma/schema.prisma | 37 + scripts/reset-password.js | 53 ++ scripts/setup-admin.js | 82 ++ 29 files changed, 2464 insertions(+), 815 deletions(-) delete mode 100644 .claude/settings.json create mode 100644 .react-router/types/app/routes/+types/api.anlagevermoegen.$id.ts create mode 100644 .react-router/types/app/routes/+types/api.anlagevermoegen.ts create mode 100644 .react-router/types/app/routes/+types/api.companies.$id.money.ts create mode 100644 .react-router/types/app/routes/+types/companies.$id.anlagevermoegen.ts create mode 100644 .react-router/types/app/routes/+types/companies.$id.money.ts create mode 100644 app/routes/api.anlagevermoegen.$id.ts create mode 100644 app/routes/api.anlagevermoegen.ts create mode 100644 app/routes/api.companies.$id.money.ts create mode 100644 app/routes/companies.$id.money.tsx create mode 100644 prisma/migrations/20260324162325_add_zahlungsart_steuersatz/migration.sql create mode 100644 prisma/migrations/20260324172641_add_money_field/migration.sql create mode 100644 prisma/migrations/20260324175608_add_buchungen_model/migration.sql create mode 100644 scripts/reset-password.js create mode 100644 scripts/setup-admin.js diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index ea3a14e..0000000 --- a/.claude/settings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(npx react-router typegen)", - "Bash(npx react-router build)" - ], - "additionalDirectories": [ - "/home/henry/.claude/projects/-home-henry-code-AnnasRechnungsManager" - ] - } -} diff --git a/.react-router/types/+routes.ts b/.react-router/types/+routes.ts index 7dc9bfe..0125122 100644 --- a/.react-router/types/+routes.ts +++ b/.react-router/types/+routes.ts @@ -88,6 +88,16 @@ type Pages = { "id": string; }; }; + "/companies/:id/anlagevermoegen": { + params: { + "id": string; + }; + }; + "/companies/:id/money": { + params: { + "id": string; + }; + }; "/archiv": { params: {}; }; @@ -129,6 +139,11 @@ type Pages = { "id": string; }; }; + "/api/companies/:id/money": { + params: { + "id": string; + }; + }; "/api/customers": { params: {}; }; @@ -185,12 +200,20 @@ type Pages = { "id": string; }; }; + "/api/anlagevermoegen": { + params: {}; + }; + "/api/anlagevermoegen/:id": { + params: { + "id": string; + }; + }; }; type RouteFiles = { "root.tsx": { id: "root"; - page: "/" | "/login" | "/logout" | "/companies" | "/companies/new" | "/companies/:id" | "/companies/:id/edit" | "/companies/:id/customers" | "/companies/:id/leistungen" | "/companies/:id/invoices" | "/companies/:id/invoices/new" | "/companies/:id/invoices/:invoiceId" | "/companies/:id/invoices/:invoiceId/edit" | "/companies/:id/reports" | "/companies/:id/bilanzen" | "/companies/:id/ausgaben" | "/companies/:id/einnahmen" | "/archiv" | "/settings/password" | "/admin/mandanten" | "/admin/users" | "/admin/users/new" | "/admin/users/:id" | "/admin/logs" | "/api/companies" | "/api/companies/:id" | "/api/companies/:id/customers" | "/api/companies/:id/invoices" | "/api/customers" | "/api/customers/:id" | "/api/services" | "/api/services/:id" | "/api/invoices" | "/api/invoices/:id" | "/api/invoices/:id/pdf" | "/api/invoices/:id/xml" | "/api/reports" | "/api/bilanzen" | "/api/ausgaben" | "/api/ausgaben/:id" | "/api/einnahmen" | "/api/einnahmen/:id"; + page: "/" | "/login" | "/logout" | "/companies" | "/companies/new" | "/companies/:id" | "/companies/:id/edit" | "/companies/:id/customers" | "/companies/:id/leistungen" | "/companies/:id/invoices" | "/companies/:id/invoices/new" | "/companies/:id/invoices/:invoiceId" | "/companies/:id/invoices/:invoiceId/edit" | "/companies/:id/reports" | "/companies/:id/bilanzen" | "/companies/:id/ausgaben" | "/companies/:id/einnahmen" | "/companies/:id/anlagevermoegen" | "/companies/:id/money" | "/archiv" | "/settings/password" | "/admin/mandanten" | "/admin/users" | "/admin/users/new" | "/admin/users/:id" | "/admin/logs" | "/api/companies" | "/api/companies/:id" | "/api/companies/:id/customers" | "/api/companies/:id/invoices" | "/api/companies/:id/money" | "/api/customers" | "/api/customers/:id" | "/api/services" | "/api/services/:id" | "/api/invoices" | "/api/invoices/:id" | "/api/invoices/:id/pdf" | "/api/invoices/:id/xml" | "/api/reports" | "/api/bilanzen" | "/api/ausgaben" | "/api/ausgaben/:id" | "/api/einnahmen" | "/api/einnahmen/:id" | "/api/anlagevermoegen" | "/api/anlagevermoegen/:id"; }; "routes/login.tsx": { id: "routes/login"; @@ -202,7 +225,7 @@ type RouteFiles = { }; "routes/dashboard-layout.tsx": { id: "routes/dashboard-layout"; - page: "/" | "/companies" | "/companies/new" | "/companies/:id" | "/companies/:id/edit" | "/companies/:id/customers" | "/companies/:id/leistungen" | "/companies/:id/invoices" | "/companies/:id/invoices/new" | "/companies/:id/invoices/:invoiceId" | "/companies/:id/invoices/:invoiceId/edit" | "/companies/:id/reports" | "/companies/:id/bilanzen" | "/companies/:id/ausgaben" | "/companies/:id/einnahmen" | "/archiv" | "/settings/password"; + page: "/" | "/companies" | "/companies/new" | "/companies/:id" | "/companies/:id/edit" | "/companies/:id/customers" | "/companies/:id/leistungen" | "/companies/:id/invoices" | "/companies/:id/invoices/new" | "/companies/:id/invoices/:invoiceId" | "/companies/:id/invoices/:invoiceId/edit" | "/companies/:id/reports" | "/companies/:id/bilanzen" | "/companies/:id/ausgaben" | "/companies/:id/einnahmen" | "/companies/:id/anlagevermoegen" | "/companies/:id/money" | "/archiv" | "/settings/password"; }; "routes/home.tsx": { id: "routes/home"; @@ -264,6 +287,14 @@ type RouteFiles = { id: "routes/companies.$id.einnahmen"; page: "/companies/:id/einnahmen"; }; + "routes/companies.$id.anlagevermoegen.tsx": { + id: "routes/companies.$id.anlagevermoegen"; + page: "/companies/:id/anlagevermoegen"; + }; + "routes/companies.$id.money.tsx": { + id: "routes/companies.$id.money"; + page: "/companies/:id/money"; + }; "routes/archiv.tsx": { id: "routes/archiv"; page: "/archiv"; @@ -312,6 +343,10 @@ type RouteFiles = { id: "routes/api.companies.$id.invoices"; page: "/api/companies/:id/invoices"; }; + "routes/api.companies.$id.money.ts": { + id: "routes/api.companies.$id.money"; + page: "/api/companies/:id/money"; + }; "routes/api.customers.ts": { id: "routes/api.customers"; page: "/api/customers"; @@ -368,6 +403,14 @@ type RouteFiles = { id: "routes/api.einnahmen.$id"; page: "/api/einnahmen/:id"; }; + "routes/api.anlagevermoegen.ts": { + id: "routes/api.anlagevermoegen"; + page: "/api/anlagevermoegen"; + }; + "routes/api.anlagevermoegen.$id.ts": { + id: "routes/api.anlagevermoegen.$id"; + page: "/api/anlagevermoegen/:id"; + }; }; type RouteModules = { @@ -390,6 +433,8 @@ type RouteModules = { "routes/companies.$id.bilanzen": typeof import("./app/routes/companies.$id.bilanzen.tsx"); "routes/companies.$id.ausgaben": typeof import("./app/routes/companies.$id.ausgaben.tsx"); "routes/companies.$id.einnahmen": typeof import("./app/routes/companies.$id.einnahmen.tsx"); + "routes/companies.$id.anlagevermoegen": typeof import("./app/routes/companies.$id.anlagevermoegen.tsx"); + "routes/companies.$id.money": typeof import("./app/routes/companies.$id.money.tsx"); "routes/archiv": typeof import("./app/routes/archiv.tsx"); "routes/settings.password": typeof import("./app/routes/settings.password.tsx"); "routes/admin-layout": typeof import("./app/routes/admin-layout.tsx"); @@ -402,6 +447,7 @@ type RouteModules = { "routes/api.companies.$id": typeof import("./app/routes/api.companies.$id.ts"); "routes/api.companies.$id.customers": typeof import("./app/routes/api.companies.$id.customers.ts"); "routes/api.companies.$id.invoices": typeof import("./app/routes/api.companies.$id.invoices.ts"); + "routes/api.companies.$id.money": typeof import("./app/routes/api.companies.$id.money.ts"); "routes/api.customers": typeof import("./app/routes/api.customers.ts"); "routes/api.customers.$id": typeof import("./app/routes/api.customers.$id.ts"); "routes/api.services": typeof import("./app/routes/api.services.ts"); @@ -416,4 +462,6 @@ type RouteModules = { "routes/api.ausgaben.$id": typeof import("./app/routes/api.ausgaben.$id.ts"); "routes/api.einnahmen": typeof import("./app/routes/api.einnahmen.ts"); "routes/api.einnahmen.$id": typeof import("./app/routes/api.einnahmen.$id.ts"); + "routes/api.anlagevermoegen": typeof import("./app/routes/api.anlagevermoegen.ts"); + "routes/api.anlagevermoegen.$id": typeof import("./app/routes/api.anlagevermoegen.$id.ts"); }; \ No newline at end of file diff --git a/.react-router/types/app/routes/+types/api.anlagevermoegen.$id.ts b/.react-router/types/app/routes/+types/api.anlagevermoegen.$id.ts new file mode 100644 index 0000000..338667f --- /dev/null +++ b/.react-router/types/app/routes/+types/api.anlagevermoegen.$id.ts @@ -0,0 +1,62 @@ +// Generated by React Router + +import type { GetInfo, GetAnnotations } from "react-router/internal"; + +type Module = typeof import("../api.anlagevermoegen.$id.js") + +type Info = GetInfo<{ + file: "routes/api.anlagevermoegen.$id.ts", + module: Module +}> + +type Matches = [{ + id: "root"; + module: typeof import("../../root.js"); +}, { + id: "routes/api.anlagevermoegen.$id"; + module: typeof import("../api.anlagevermoegen.$id.js"); +}]; + +type Annotations = GetAnnotations; + +export namespace Route { + // links + export type LinkDescriptors = Annotations["LinkDescriptors"]; + export type LinksFunction = Annotations["LinksFunction"]; + + // meta + export type MetaArgs = Annotations["MetaArgs"]; + export type MetaDescriptors = Annotations["MetaDescriptors"]; + export type MetaFunction = Annotations["MetaFunction"]; + + // headers + export type HeadersArgs = Annotations["HeadersArgs"]; + export type HeadersFunction = Annotations["HeadersFunction"]; + + // middleware + export type MiddlewareFunction = Annotations["MiddlewareFunction"]; + + // clientMiddleware + export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"]; + + // loader + export type LoaderArgs = Annotations["LoaderArgs"]; + + // clientLoader + export type ClientLoaderArgs = Annotations["ClientLoaderArgs"]; + + // action + export type ActionArgs = Annotations["ActionArgs"]; + + // clientAction + export type ClientActionArgs = Annotations["ClientActionArgs"]; + + // HydrateFallback + export type HydrateFallbackProps = Annotations["HydrateFallbackProps"]; + + // Component + export type ComponentProps = Annotations["ComponentProps"]; + + // ErrorBoundary + export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"]; +} \ No newline at end of file diff --git a/.react-router/types/app/routes/+types/api.anlagevermoegen.ts b/.react-router/types/app/routes/+types/api.anlagevermoegen.ts new file mode 100644 index 0000000..4b2a5fa --- /dev/null +++ b/.react-router/types/app/routes/+types/api.anlagevermoegen.ts @@ -0,0 +1,62 @@ +// Generated by React Router + +import type { GetInfo, GetAnnotations } from "react-router/internal"; + +type Module = typeof import("../api.anlagevermoegen.js") + +type Info = GetInfo<{ + file: "routes/api.anlagevermoegen.ts", + module: Module +}> + +type Matches = [{ + id: "root"; + module: typeof import("../../root.js"); +}, { + id: "routes/api.anlagevermoegen"; + module: typeof import("../api.anlagevermoegen.js"); +}]; + +type Annotations = GetAnnotations; + +export namespace Route { + // links + export type LinkDescriptors = Annotations["LinkDescriptors"]; + export type LinksFunction = Annotations["LinksFunction"]; + + // meta + export type MetaArgs = Annotations["MetaArgs"]; + export type MetaDescriptors = Annotations["MetaDescriptors"]; + export type MetaFunction = Annotations["MetaFunction"]; + + // headers + export type HeadersArgs = Annotations["HeadersArgs"]; + export type HeadersFunction = Annotations["HeadersFunction"]; + + // middleware + export type MiddlewareFunction = Annotations["MiddlewareFunction"]; + + // clientMiddleware + export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"]; + + // loader + export type LoaderArgs = Annotations["LoaderArgs"]; + + // clientLoader + export type ClientLoaderArgs = Annotations["ClientLoaderArgs"]; + + // action + export type ActionArgs = Annotations["ActionArgs"]; + + // clientAction + export type ClientActionArgs = Annotations["ClientActionArgs"]; + + // HydrateFallback + export type HydrateFallbackProps = Annotations["HydrateFallbackProps"]; + + // Component + export type ComponentProps = Annotations["ComponentProps"]; + + // ErrorBoundary + export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"]; +} \ No newline at end of file diff --git a/.react-router/types/app/routes/+types/api.companies.$id.money.ts b/.react-router/types/app/routes/+types/api.companies.$id.money.ts new file mode 100644 index 0000000..e967c83 --- /dev/null +++ b/.react-router/types/app/routes/+types/api.companies.$id.money.ts @@ -0,0 +1,62 @@ +// Generated by React Router + +import type { GetInfo, GetAnnotations } from "react-router/internal"; + +type Module = typeof import("../api.companies.$id.money.js") + +type Info = GetInfo<{ + file: "routes/api.companies.$id.money.ts", + module: Module +}> + +type Matches = [{ + id: "root"; + module: typeof import("../../root.js"); +}, { + id: "routes/api.companies.$id.money"; + module: typeof import("../api.companies.$id.money.js"); +}]; + +type Annotations = GetAnnotations; + +export namespace Route { + // links + export type LinkDescriptors = Annotations["LinkDescriptors"]; + export type LinksFunction = Annotations["LinksFunction"]; + + // meta + export type MetaArgs = Annotations["MetaArgs"]; + export type MetaDescriptors = Annotations["MetaDescriptors"]; + export type MetaFunction = Annotations["MetaFunction"]; + + // headers + export type HeadersArgs = Annotations["HeadersArgs"]; + export type HeadersFunction = Annotations["HeadersFunction"]; + + // middleware + export type MiddlewareFunction = Annotations["MiddlewareFunction"]; + + // clientMiddleware + export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"]; + + // loader + export type LoaderArgs = Annotations["LoaderArgs"]; + + // clientLoader + export type ClientLoaderArgs = Annotations["ClientLoaderArgs"]; + + // action + export type ActionArgs = Annotations["ActionArgs"]; + + // clientAction + export type ClientActionArgs = Annotations["ClientActionArgs"]; + + // HydrateFallback + export type HydrateFallbackProps = Annotations["HydrateFallbackProps"]; + + // Component + export type ComponentProps = Annotations["ComponentProps"]; + + // ErrorBoundary + export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"]; +} \ No newline at end of file diff --git a/.react-router/types/app/routes/+types/companies.$id.anlagevermoegen.ts b/.react-router/types/app/routes/+types/companies.$id.anlagevermoegen.ts new file mode 100644 index 0000000..9c2c853 --- /dev/null +++ b/.react-router/types/app/routes/+types/companies.$id.anlagevermoegen.ts @@ -0,0 +1,65 @@ +// Generated by React Router + +import type { GetInfo, GetAnnotations } from "react-router/internal"; + +type Module = typeof import("../companies.$id.anlagevermoegen.js") + +type Info = GetInfo<{ + file: "routes/companies.$id.anlagevermoegen.tsx", + module: Module +}> + +type Matches = [{ + id: "root"; + module: typeof import("../../root.js"); +}, { + id: "routes/dashboard-layout"; + module: typeof import("../dashboard-layout.js"); +}, { + id: "routes/companies.$id.anlagevermoegen"; + module: typeof import("../companies.$id.anlagevermoegen.js"); +}]; + +type Annotations = GetAnnotations; + +export namespace Route { + // links + export type LinkDescriptors = Annotations["LinkDescriptors"]; + export type LinksFunction = Annotations["LinksFunction"]; + + // meta + export type MetaArgs = Annotations["MetaArgs"]; + export type MetaDescriptors = Annotations["MetaDescriptors"]; + export type MetaFunction = Annotations["MetaFunction"]; + + // headers + export type HeadersArgs = Annotations["HeadersArgs"]; + export type HeadersFunction = Annotations["HeadersFunction"]; + + // middleware + export type MiddlewareFunction = Annotations["MiddlewareFunction"]; + + // clientMiddleware + export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"]; + + // loader + export type LoaderArgs = Annotations["LoaderArgs"]; + + // clientLoader + export type ClientLoaderArgs = Annotations["ClientLoaderArgs"]; + + // action + export type ActionArgs = Annotations["ActionArgs"]; + + // clientAction + export type ClientActionArgs = Annotations["ClientActionArgs"]; + + // HydrateFallback + export type HydrateFallbackProps = Annotations["HydrateFallbackProps"]; + + // Component + export type ComponentProps = Annotations["ComponentProps"]; + + // ErrorBoundary + export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"]; +} \ No newline at end of file diff --git a/.react-router/types/app/routes/+types/companies.$id.money.ts b/.react-router/types/app/routes/+types/companies.$id.money.ts new file mode 100644 index 0000000..710cead --- /dev/null +++ b/.react-router/types/app/routes/+types/companies.$id.money.ts @@ -0,0 +1,65 @@ +// Generated by React Router + +import type { GetInfo, GetAnnotations } from "react-router/internal"; + +type Module = typeof import("../companies.$id.money.js") + +type Info = GetInfo<{ + file: "routes/companies.$id.money.tsx", + module: Module +}> + +type Matches = [{ + id: "root"; + module: typeof import("../../root.js"); +}, { + id: "routes/dashboard-layout"; + module: typeof import("../dashboard-layout.js"); +}, { + id: "routes/companies.$id.money"; + module: typeof import("../companies.$id.money.js"); +}]; + +type Annotations = GetAnnotations; + +export namespace Route { + // links + export type LinkDescriptors = Annotations["LinkDescriptors"]; + export type LinksFunction = Annotations["LinksFunction"]; + + // meta + export type MetaArgs = Annotations["MetaArgs"]; + export type MetaDescriptors = Annotations["MetaDescriptors"]; + export type MetaFunction = Annotations["MetaFunction"]; + + // headers + export type HeadersArgs = Annotations["HeadersArgs"]; + export type HeadersFunction = Annotations["HeadersFunction"]; + + // middleware + export type MiddlewareFunction = Annotations["MiddlewareFunction"]; + + // clientMiddleware + export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"]; + + // loader + export type LoaderArgs = Annotations["LoaderArgs"]; + + // clientLoader + export type ClientLoaderArgs = Annotations["ClientLoaderArgs"]; + + // action + export type ActionArgs = Annotations["ActionArgs"]; + + // clientAction + export type ClientActionArgs = Annotations["ClientActionArgs"]; + + // HydrateFallback + export type HydrateFallbackProps = Annotations["HydrateFallbackProps"]; + + // Component + export type ComponentProps = Annotations["ComponentProps"]; + + // ErrorBoundary + export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"]; +} \ No newline at end of file diff --git a/app/entry.server.tsx b/app/entry.server.tsx index 89d5430..5075f22 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -1,4 +1,4 @@ -import { startCleanupScheduler } from "@/lib/cleanup.server"; +import { startCleanupScheduler } from "./lib/cleanup.server"; import { PassThrough } from "node:stream"; import type { AppLoadContext, EntryContext } from "react-router"; import { createReadableStreamFromReadable } from "@react-router/node"; diff --git a/app/routes.ts b/app/routes.ts index 0b2a06d..f214797 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -20,6 +20,8 @@ export default [ route("companies/:id/bilanzen", "routes/companies.$id.bilanzen.tsx"), route("companies/:id/ausgaben", "routes/companies.$id.ausgaben.tsx"), route("companies/:id/einnahmen", "routes/companies.$id.einnahmen.tsx"), + route("companies/:id/anlagevermoegen", "routes/companies.$id.anlagevermoegen.tsx"), + route("companies/:id/money", "routes/companies.$id.money.tsx"), route("archiv", "routes/archiv.tsx"), route("settings/password", "routes/settings.password.tsx"), ]), @@ -38,6 +40,7 @@ export default [ route("api/companies/:id", "routes/api.companies.$id.ts"), route("api/companies/:id/customers", "routes/api.companies.$id.customers.ts"), route("api/companies/:id/invoices", "routes/api.companies.$id.invoices.ts"), + route("api/companies/:id/money", "routes/api.companies.$id.money.ts"), route("api/customers", "routes/api.customers.ts"), route("api/customers/:id", "routes/api.customers.$id.ts"), route("api/services", "routes/api.services.ts"), @@ -52,4 +55,7 @@ export default [ route("api/ausgaben/:id", "routes/api.ausgaben.$id.ts"), route("api/einnahmen", "routes/api.einnahmen.ts"), route("api/einnahmen/:id", "routes/api.einnahmen.$id.ts"), + route("api/anlagevermoegen", "routes/api.anlagevermoegen.ts"), + route("api/anlagevermoegen/:id", "routes/api.anlagevermoegen.$id.ts"), + ] satisfies RouteConfig; diff --git a/app/routes/api.anlagevermoegen.$id.ts b/app/routes/api.anlagevermoegen.$id.ts new file mode 100644 index 0000000..076878a --- /dev/null +++ b/app/routes/api.anlagevermoegen.$id.ts @@ -0,0 +1,52 @@ +import { getApiUser } from "@/session.server"; +import prisma from "@/lib/prisma.server"; +import { z } from "zod"; + +const updateSchema = z.object({ + bezeichnung: z.string().min(1), + anschaffungsdatum: z.string().min(1), + anschaffungskosten: z.number().positive(), + nutzungsdauerJahre: z.number().int().min(1), + restwert: z.number().min(0), + beschreibung: z.string().optional(), + aktiv: z.boolean(), +}); + +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 asset = await prisma.anlagegut.findFirst({ + where: { id: params.id, company: { userId: user.id } }, + }); + if (!asset) return Response.json({ error: "Not found" }, { status: 404 }); + + if (request.method === "DELETE") { + await prisma.anlagegut.delete({ where: { id: params.id } }); + return Response.json({ ok: true }); + } + + const body = await request.json(); + const parsed = updateSchema.safeParse(body); + if (!parsed.success) return Response.json({ error: parsed.error.issues }, { status: 400 }); + + const updated = await prisma.anlagegut.update({ + where: { id: params.id }, + data: { + bezeichnung: parsed.data.bezeichnung, + anschaffungsdatum: new Date(parsed.data.anschaffungsdatum), + anschaffungskosten: parsed.data.anschaffungskosten, + nutzungsdauerJahre: parsed.data.nutzungsdauerJahre, + restwert: parsed.data.restwert, + beschreibung: parsed.data.beschreibung, + aktiv: parsed.data.aktiv, + }, + }); + + return Response.json({ + ...updated, + anschaffungskosten: Number(updated.anschaffungskosten), + restwert: Number(updated.restwert), + anschaffungsdatum: updated.anschaffungsdatum.toISOString(), + }); +} diff --git a/app/routes/api.anlagevermoegen.ts b/app/routes/api.anlagevermoegen.ts new file mode 100644 index 0000000..bbf0918 --- /dev/null +++ b/app/routes/api.anlagevermoegen.ts @@ -0,0 +1,104 @@ +import { getApiUser } from "@/session.server"; +import prisma from "@/lib/prisma.server"; +import { z } from "zod"; +import { afaFuerJahr, buchwert, assetStatus } from "@/lib/afa"; + +const createSchema = z.object({ + companyId: z.string().min(1), + bezeichnung: z.string().min(1), + anschaffungsdatum: z.string().min(1), + anschaffungskosten: z.number().positive(), + nutzungsdauerJahre: z.number().int().min(1), + restwert: z.number().min(0).default(0), + beschreibung: z.string().optional(), + aktiv: z.boolean().default(true), +}); + +function toRaw(a: { + anschaffungskosten: unknown; + nutzungsdauerJahre: number; + restwert: unknown; + anschaffungsdatum: Date; + aktiv: boolean; +}) { + return { + anschaffungskosten: Number(a.anschaffungskosten), + nutzungsdauerJahre: a.nutzungsdauerJahre, + restwert: Number(a.restwert), + anschaffungsdatum: a.anschaffungsdatum.toISOString(), + aktiv: a.aktiv, + }; +} + +export async function loader({ request }: { request: Request }) { + const user = await getApiUser(request); + if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 }); + + const { searchParams } = new URL(request.url); + const companyId = searchParams.get("companyId"); + const year = parseInt(searchParams.get("year") ?? String(new Date().getFullYear())); + + if (!companyId) return Response.json({ error: "companyId required" }, { status: 400 }); + + const company = await prisma.company.findFirst({ where: { id: companyId, userId: user.id } }); + if (!company) return Response.json({ error: "Not found" }, { status: 404 }); + + const assets = await prisma.anlagegut.findMany({ + where: { companyId }, + orderBy: { anschaffungsdatum: "asc" }, + }); + + return Response.json({ + year, + assets: assets.map((a) => { + const raw = toRaw(a); + return { + id: a.id, + bezeichnung: a.bezeichnung, + beschreibung: a.beschreibung, + anschaffungsdatum: a.anschaffungsdatum.toISOString(), + anschaffungskosten: raw.anschaffungskosten, + nutzungsdauerJahre: a.nutzungsdauerJahre, + restwert: raw.restwert, + aktiv: a.aktiv, + afaJahr: afaFuerJahr(raw, year), + buchwert: buchwert(raw, year), + status: assetStatus(raw, year), + }; + }), + }); +} + +export async function action({ request }: { request: Request }) { + const user = await getApiUser(request); + if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 }); + + const body = await request.json(); + const parsed = createSchema.safeParse(body); + if (!parsed.success) return Response.json({ error: parsed.error.issues }, { status: 400 }); + + const company = await prisma.company.findFirst({ + where: { id: parsed.data.companyId, userId: user.id }, + }); + if (!company) return Response.json({ error: "Company not found" }, { status: 404 }); + + const asset = await prisma.anlagegut.create({ + data: { + companyId: parsed.data.companyId, + bezeichnung: parsed.data.bezeichnung, + anschaffungsdatum: new Date(parsed.data.anschaffungsdatum), + anschaffungskosten: parsed.data.anschaffungskosten, + nutzungsdauerJahre: parsed.data.nutzungsdauerJahre, + restwert: parsed.data.restwert, + beschreibung: parsed.data.beschreibung, + aktiv: parsed.data.aktiv, + }, + }); + + return Response.json({ + ...asset, + anschaffungskosten: Number(asset.anschaffungskosten), + restwert: Number(asset.restwert), + anschaffungsdatum: asset.anschaffungsdatum.toISOString(), + }, { status: 201 }); +} diff --git a/app/routes/api.ausgaben.$id.ts b/app/routes/api.ausgaben.$id.ts index 6130f69..957bd34 100644 --- a/app/routes/api.ausgaben.$id.ts +++ b/app/routes/api.ausgaben.$id.ts @@ -6,6 +6,8 @@ import { AusgabeKategorie } from "@prisma/client"; const updateSchema = z.object({ kategorie: z.nativeEnum(AusgabeKategorie), betrag: z.number().positive(), + steuersatz: z.number().min(0).default(0), + zahlungsart: z.enum(["KASSE", "BANK"]).default("BANK"), datum: z.string().min(1), beschreibung: z.string().optional(), }); @@ -33,10 +35,17 @@ export async function action({ request, params }: { request: Request; params: { data: { 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, }, }); - return Response.json({ ...updated, betrag: Number(updated.betrag), datum: updated.datum.toISOString() }); + return Response.json({ + ...updated, + betrag: Number(updated.betrag), + steuersatz: Number(updated.steuersatz), + datum: updated.datum.toISOString(), + }); } diff --git a/app/routes/api.ausgaben.ts b/app/routes/api.ausgaben.ts index 0054ad0..1523574 100644 --- a/app/routes/api.ausgaben.ts +++ b/app/routes/api.ausgaben.ts @@ -7,6 +7,8 @@ const createSchema = z.object({ companyId: z.string().min(1), kategorie: z.nativeEnum(AusgabeKategorie), betrag: z.number().positive(), + steuersatz: z.number().min(0).default(0), + zahlungsart: z.enum(["KASSE", "BANK"]).default("BANK"), datum: z.string().min(1), beschreibung: z.string().optional(), }); @@ -41,6 +43,7 @@ export async function loader({ request }: { request: Request }) { ausgaben.map((a) => ({ ...a, betrag: Number(a.betrag), + steuersatz: Number(a.steuersatz), datum: a.datum.toISOString(), })) ); @@ -64,10 +67,17 @@ export async function action({ request }: { request: Request }) { companyId: parsed.data.companyId, 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, }, }); - return Response.json({ ...ausgabe, betrag: Number(ausgabe.betrag), datum: ausgabe.datum.toISOString() }, { status: 201 }); + return Response.json({ + ...ausgabe, + betrag: Number(ausgabe.betrag), + steuersatz: Number(ausgabe.steuersatz), + datum: ausgabe.datum.toISOString(), + }, { status: 201 }); } diff --git a/app/routes/api.bilanzen.ts b/app/routes/api.bilanzen.ts index 71a4c5f..60980bb 100644 --- a/app/routes/api.bilanzen.ts +++ b/app/routes/api.bilanzen.ts @@ -64,26 +64,47 @@ export async function loader({ request }: { request: Request }) { const summeAktiva = forderungen + bank; // Betriebsausgaben für das Jahr - const ausgabenAgg = await prisma.betriebsausgabe.aggregate({ + const ausgaben = await prisma.betriebsausgabe.findMany({ where: { companyId, datum: { gte: yearStart, lt: yearEnd } }, - _sum: { betrag: true }, - _count: true, }); - const ausgabenByKategorie = await prisma.betriebsausgabe.groupBy({ - by: ["kategorie"], - where: { companyId, datum: { gte: yearStart, lt: yearEnd } }, - _sum: { betrag: true }, - }); + const ausgabenGesamt = ausgaben.reduce((s, a) => s + Number(a.betrag), 0); + const ausgabenVorsteuer = ausgaben.reduce((s, a) => { + const brutto = Number(a.betrag); + const rate = Number(a.steuersatz) / 100; + return s + (rate > 0 ? Math.round((brutto / (1 + rate)) * rate * 100) / 100 : 0); + }, 0); - const ausgabenGesamt = Number(ausgabenAgg._sum.betrag ?? 0); + // Ausgaben nach Kategorie + const ausgabenByKategorieMap: Record = {}; + for (const a of ausgaben) { + const k = a.kategorie; + ausgabenByKategorieMap[k] = (ausgabenByKategorieMap[k] ?? 0) + Number(a.betrag); + } + const ausgabenByKategorie = Object.entries(ausgabenByKategorieMap).map(([kategorie, betrag]) => ({ kategorie, betrag })); // Sonstige Einnahmen für das Jahr - const einnahmenAgg = await prisma.betriebseinnahme.aggregate({ + const einnahmen = await prisma.betriebseinnahme.findMany({ where: { companyId, datum: { gte: yearStart, lt: yearEnd } }, - _sum: { betrag: true }, }); - const sonstigeEinnahmen = Number(einnahmenAgg._sum.betrag ?? 0); + const sonstigeEinnahmen = einnahmen.reduce((s, e) => s + Number(e.betrag), 0); + const einnahmenUst = einnahmen.reduce((s, e) => { + const brutto = Number(e.betrag); + const rate = Number(e.steuersatz) / 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); + + // Kasse-Saldo = bezahlte Rechnungen (Kasse-Anteil wird nicht getrennt) + sonstige Einnahmen Kasse - Ausgaben Kasse + // Bank-Näherung = bezahlte Rechnungen + sonstige Einnahmen Bank - Ausgaben Bank + const kasseNetto = einnahmenKasse - ausgabenKasse; + const bankNetto = bank + einnahmenBank - ausgabenBank; + const summeAktivaErweitert = forderungen + Math.max(0, bankNetto) + Math.max(0, kasseNetto); const jahresergebnis = guvNetto + sonstigeEinnahmen - ausgabenGesamt; @@ -97,22 +118,22 @@ export async function loader({ request }: { request: Request }) { grossTotal: guvBrutto, invoiceCount: guvInvoices.length, ausgabenGesamt, - ausgabenByKategorie: ausgabenByKategorie.map((a) => ({ - kategorie: a.kategorie, - betrag: Number(a._sum.betrag ?? 0), - })), + ausgabenVorsteuer, + ausgabenByKategorie, sonstigeEinnahmen, + einnahmenUst, jahresergebnis, }, bilanz: { aktiva: { forderungen: { betrag: forderungen, anzahl: forderungenAgg._count }, - bank: { betrag: bank, anzahl: bankAgg._count }, - summe: summeAktiva, + bank: { betrag: Math.max(0, bankNetto), anzahl: bankAgg._count }, + kasse: { betrag: Math.max(0, kasseNetto) }, + summe: summeAktivaErweitert, }, passiva: { - eigenkapital: summeAktiva, - summe: summeAktiva, + eigenkapital: summeAktivaErweitert, + summe: summeAktivaErweitert, }, }, }); diff --git a/app/routes/api.companies.$id.money.ts b/app/routes/api.companies.$id.money.ts new file mode 100644 index 0000000..eed2fa8 --- /dev/null +++ b/app/routes/api.companies.$id.money.ts @@ -0,0 +1,110 @@ +import { getApiUser } from "@/session.server"; +import prisma from "@/lib/prisma.server"; + +type Transaction = { + id: string; + date: string; + account: "kasse" | "bank"; + type: "einlage" | "entnahme"; + amount: number; + description: string; +}; + +function toTransaction(buchung: { id: string; date: Date; account: "KASSE" | "BANK"; type: "EINLAGE" | "ENTNAHME"; amount: { toString(): string } | number | string; description: string | null | undefined; }): Transaction { + return { + id: buchung.id, + date: buchung.date.toISOString().split("T")[0], + account: buchung.account === "KASSE" ? "kasse" : "bank", + type: buchung.type === "EINLAGE" ? "einlage" : "entnahme", + amount: Number(buchung.amount), + description: buchung.description || "", + }; +} + +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 { id } = params; + + const company = await prisma.company.findFirst({ + where: { id, userId: user.id }, + }); + + if (!company) return Response.json({ error: "Company not found" }, { status: 404 }); + + const buchungen = await prisma.buchung.findMany({ + where: { companyId: id }, + orderBy: { date: "desc" }, + }); + + const transactions = buchungen.map(toTransaction); + const balance = transactions.reduce((sum: number, t: Transaction) => sum + (t.type === "einlage" ? t.amount : -t.amount), 0 as number); + + return Response.json({ transactions, balance }); +} + +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 { id } = params; + + const company = await prisma.company.findFirst({ + where: { id, userId: user.id }, + }); + if (!company) return Response.json({ error: "Company not found" }, { status: 404 }); + + const url = new URL(request.url); + const transactionId = url.searchParams.get("transactionId"); + const method = request.method; + const data = await request.json().catch(() => ({})); + + if (method === "POST") { + const amount = Number(data.amount); + if (!data.date || !data.account || !data.type || Number.isNaN(amount) || amount <= 0) { + return Response.json({ error: "Ungültige Daten" }, { status: 400 }); + } + + await prisma.buchung.create({ + data: { + companyId: id, + date: new Date(data.date), + account: data.account === "bank" ? "BANK" : "KASSE", + type: data.type === "entnahme" ? "ENTNAHME" : "EINLAGE", + amount: amount, + description: data.description || "", + }, + }); + } else if (method === "PUT") { + if (!transactionId) return Response.json({ error: "transactionId required" }, { status: 400 }); + const amount = Number(data.amount); + if (!data.date || !data.account || !data.type || Number.isNaN(amount) || amount <= 0) { + return Response.json({ error: "Ungültige Daten" }, { status: 400 }); + } + + const exist = await prisma.buchung.findFirst({ where: { id: transactionId, companyId: id } }); + if (!exist) return Response.json({ error: "Transaction not found" }, { status: 404 }); + + await prisma.buchung.update({ + where: { id: transactionId }, + data: { + date: new Date(data.date), + account: data.account === "bank" ? "BANK" : "KASSE", + type: data.type === "entnahme" ? "ENTNAHME" : "EINLAGE", + amount: amount, + description: data.description || "", + }, + }); + } else if (method === "DELETE") { + if (!transactionId) return Response.json({ error: "transactionId required" }, { status: 400 }); + + 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 transactions = buchungen.map(toTransaction); + const balance = transactions.reduce((sum: number, t: Transaction) => sum + (t.type === "einlage" ? t.amount : -t.amount), 0 as number); + + return Response.json({ transactions, balance }); +} diff --git a/app/routes/api.einnahmen.$id.ts b/app/routes/api.einnahmen.$id.ts index 3251a88..e324c59 100644 --- a/app/routes/api.einnahmen.$id.ts +++ b/app/routes/api.einnahmen.$id.ts @@ -6,6 +6,8 @@ import { EinnahmeKategorie } from "@prisma/client"; const updateSchema = z.object({ kategorie: z.nativeEnum(EinnahmeKategorie), betrag: z.number().positive(), + steuersatz: z.number().min(0).default(0), + zahlungsart: z.enum(["KASSE", "BANK"]).default("BANK"), datum: z.string().min(1), beschreibung: z.string().optional(), }); @@ -46,10 +48,17 @@ export async function action({ request, params }: { request: Request; params: { data: { 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, }, }); - return Response.json({ ...updated, betrag: Number(updated.betrag), datum: updated.datum.toISOString() }); + return Response.json({ + ...updated, + betrag: Number(updated.betrag), + steuersatz: Number(updated.steuersatz), + datum: updated.datum.toISOString(), + }); } diff --git a/app/routes/api.einnahmen.ts b/app/routes/api.einnahmen.ts index 4dc0349..87a1a56 100644 --- a/app/routes/api.einnahmen.ts +++ b/app/routes/api.einnahmen.ts @@ -7,6 +7,8 @@ const createSchema = z.object({ companyId: z.string().min(1), kategorie: z.nativeEnum(EinnahmeKategorie), betrag: z.number().positive(), + steuersatz: z.number().min(0).default(0), + zahlungsart: z.enum(["KASSE", "BANK"]).default("BANK"), datum: z.string().min(1), beschreibung: z.string().optional(), }); @@ -52,6 +54,7 @@ export async function loader({ request }: { request: Request }) { einnahmen.map((e) => ({ ...e, betrag: Number(e.betrag), + steuersatz: Number(e.steuersatz), datum: e.datum.toISOString(), })) ); @@ -93,13 +96,20 @@ export async function action({ request }: { request: Request }) { companyId: parsed.data.companyId, 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, }, }); return Response.json( - { ...einnahme, betrag: Number(einnahme.betrag), datum: einnahme.datum.toISOString() }, + { + ...einnahme, + betrag: Number(einnahme.betrag), + steuersatz: Number(einnahme.steuersatz), + datum: einnahme.datum.toISOString(), + }, { status: 201 } ); } diff --git a/app/routes/companies.$id.anlagevermoegen.tsx b/app/routes/companies.$id.anlagevermoegen.tsx index 51324a7..7d3a68d 100644 --- a/app/routes/companies.$id.anlagevermoegen.tsx +++ b/app/routes/companies.$id.anlagevermoegen.tsx @@ -1,18 +1,19 @@ -import { useState, useEffect } from "react"; +import { useState } from "react"; import { Link, useLoaderData, useRevalidator } from "react-router"; import { requireUser } from "@/session.server"; import prisma from "@/lib/prisma.server"; -import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; -import { ChevronLeft, Plus, Edit, Trash2, Layers } from "lucide-react"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; -import { formatCurrency, formatDate } from "@/lib/tax"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { ChevronLeft, Plus, Pencil, Trash2, Loader2 } from "lucide-react"; +import { formatCurrency } from "@/lib/tax"; +import { afaFuerJahr, buchwert, assetStatus, type AnlagegutRaw } from "@/lib/afa"; export const handle = { breadcrumbs: (data: { companyId: string; companyName: string }) => [ @@ -22,17 +23,6 @@ export const handle = { ], }; -const schema = z.object({ - bezeichnung: z.string().min(1, "Pflichtfeld"), - anschaffungsdatum: z.string().min(1, "Pflichtfeld"), - anschaffungskosten: z.coerce.number({ invalid_type_error: "Ungültiger Betrag" }).positive("Betrag muss größer 0 sein"), - nutzungsdauerJahre: z.coerce.number().int().min(1, "Mindestens 1 Jahr"), - restwert: z.coerce.number().min(0).default(0), - beschreibung: z.string().optional(), - aktiv: z.boolean().default(true), -}); -type FormData = z.infer; - interface Asset { id: string; bezeichnung: string; @@ -42,11 +32,46 @@ interface Asset { nutzungsdauerJahre: number; restwert: number; aktiv: boolean; - afaJahr: number; - buchwert: number; - status: "aktiv" | "vollständig abgeschrieben" | "inaktiv"; } +interface AssetWithAfa extends Asset { + afaJahr: number; + buchwertJahr: number; + statusLabel: string; +} + +function enrichAsset(a: Asset, year: number): AssetWithAfa { + const raw: AnlagegutRaw = { + anschaffungskosten: a.anschaffungskosten, + nutzungsdauerJahre: a.nutzungsdauerJahre, + restwert: a.restwert, + anschaffungsdatum: a.anschaffungsdatum, + aktiv: a.aktiv, + }; + return { + ...a, + afaJahr: afaFuerJahr(raw, year), + buchwertJahr: buchwert(raw, year), + statusLabel: assetStatus(raw, year), + }; +} + +const STATUS_VARIANTS: Record = { + aktiv: "success", + "vollständig abgeschrieben": "secondary", + inaktiv: "outline", +}; + +const emptyForm = { + bezeichnung: "", + anschaffungsdatum: "", + anschaffungskosten: "", + nutzungsdauerJahre: "", + restwert: "0", + beschreibung: "", + aktiv: true, +}; + export async function loader({ request, params }: { request: Request; params: { id: string } }) { const user = await requireUser(request); const company = await prisma.company.findFirst({ @@ -54,136 +79,138 @@ export async function loader({ request, params }: { request: Request; params: { select: { id: true, name: true }, }); if (!company) throw new Response("Not Found", { status: 404 }); - return { companyId: company.id, companyName: company.name }; -} -function AnlagegutForm({ - defaultValues, - onSubmit, - submitLabel, -}: { - defaultValues?: Partial; - onSubmit: (d: FormData) => Promise; - submitLabel: string; -}) { - const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm({ - resolver: zodResolver(schema), - defaultValues: { - aktiv: true, - restwert: 0, - anschaffungsdatum: new Date().toISOString().slice(0, 10), - ...defaultValues, - }, + const assets = await prisma.anlagegut.findMany({ + where: { companyId: params.id }, + orderBy: { anschaffungsdatum: "asc" }, }); - return ( -
-
- - - {errors.bezeichnung &&

{errors.bezeichnung.message}

} -
-
-
- - - {errors.anschaffungsdatum &&

{errors.anschaffungsdatum.message}

} -
-
- - - {errors.nutzungsdauerJahre &&

{errors.nutzungsdauerJahre.message}

} -
-
-
-
- - - {errors.anschaffungskosten &&

{errors.anschaffungskosten.message}

} -
-
- - - {errors.restwert &&

{errors.restwert.message}

} -
-
-
- -