diff --git a/.react-router/types/+routes.ts b/.react-router/types/+routes.ts index 4e1b4e8..7dc9bfe 100644 --- a/.react-router/types/+routes.ts +++ b/.react-router/types/+routes.ts @@ -73,12 +73,30 @@ type Pages = { "id": string; }; }; + "/companies/:id/bilanzen": { + params: { + "id": string; + }; + }; + "/companies/:id/ausgaben": { + params: { + "id": string; + }; + }; + "/companies/:id/einnahmen": { + params: { + "id": string; + }; + }; "/archiv": { params: {}; }; "/settings/password": { params: {}; }; + "/admin/mandanten": { + params: {}; + }; "/admin/users": { params: {}; }; @@ -148,12 +166,31 @@ type Pages = { "/api/reports": { params: {}; }; + "/api/bilanzen": { + params: {}; + }; + "/api/ausgaben": { + params: {}; + }; + "/api/ausgaben/:id": { + params: { + "id": string; + }; + }; + "/api/einnahmen": { + params: {}; + }; + "/api/einnahmen/: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" | "/archiv" | "/settings/password" | "/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"; + 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"; }; "routes/login.tsx": { id: "routes/login"; @@ -165,7 +202,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" | "/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" | "/archiv" | "/settings/password"; }; "routes/home.tsx": { id: "routes/home"; @@ -215,6 +252,18 @@ type RouteFiles = { id: "routes/companies.$id.reports"; page: "/companies/:id/reports"; }; + "routes/companies.$id.bilanzen.tsx": { + id: "routes/companies.$id.bilanzen"; + page: "/companies/:id/bilanzen"; + }; + "routes/companies.$id.ausgaben.tsx": { + id: "routes/companies.$id.ausgaben"; + page: "/companies/:id/ausgaben"; + }; + "routes/companies.$id.einnahmen.tsx": { + id: "routes/companies.$id.einnahmen"; + page: "/companies/:id/einnahmen"; + }; "routes/archiv.tsx": { id: "routes/archiv"; page: "/archiv"; @@ -225,7 +274,11 @@ type RouteFiles = { }; "routes/admin-layout.tsx": { id: "routes/admin-layout"; - page: "/admin/users" | "/admin/users/new" | "/admin/users/:id" | "/admin/logs"; + page: "/admin/mandanten" | "/admin/users" | "/admin/users/new" | "/admin/users/:id" | "/admin/logs"; + }; + "routes/admin.mandanten.tsx": { + id: "routes/admin.mandanten"; + page: "/admin/mandanten"; }; "routes/admin.users.tsx": { id: "routes/admin.users"; @@ -295,6 +348,26 @@ type RouteFiles = { id: "routes/api.reports"; page: "/api/reports"; }; + "routes/api.bilanzen.ts": { + id: "routes/api.bilanzen"; + page: "/api/bilanzen"; + }; + "routes/api.ausgaben.ts": { + id: "routes/api.ausgaben"; + page: "/api/ausgaben"; + }; + "routes/api.ausgaben.$id.ts": { + id: "routes/api.ausgaben.$id"; + page: "/api/ausgaben/:id"; + }; + "routes/api.einnahmen.ts": { + id: "routes/api.einnahmen"; + page: "/api/einnahmen"; + }; + "routes/api.einnahmen.$id.ts": { + id: "routes/api.einnahmen.$id"; + page: "/api/einnahmen/:id"; + }; }; type RouteModules = { @@ -314,9 +387,13 @@ type RouteModules = { "routes/companies.$id.invoices.$invoiceId": typeof import("./app/routes/companies.$id.invoices.$invoiceId.tsx"); "routes/companies.$id.invoices.$invoiceId.edit": typeof import("./app/routes/companies.$id.invoices.$invoiceId.edit.tsx"); "routes/companies.$id.reports": typeof import("./app/routes/companies.$id.reports.tsx"); + "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/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"); + "routes/admin.mandanten": typeof import("./app/routes/admin.mandanten.tsx"); "routes/admin.users": typeof import("./app/routes/admin.users.tsx"); "routes/admin.users.new": typeof import("./app/routes/admin.users.new.tsx"); "routes/admin.users.$id": typeof import("./app/routes/admin.users.$id.tsx"); @@ -334,4 +411,9 @@ type RouteModules = { "routes/api.invoices.$id.pdf": typeof import("./app/routes/api.invoices.$id.pdf.ts"); "routes/api.invoices.$id.xml": typeof import("./app/routes/api.invoices.$id.xml.ts"); "routes/api.reports": typeof import("./app/routes/api.reports.ts"); + "routes/api.bilanzen": typeof import("./app/routes/api.bilanzen.ts"); + "routes/api.ausgaben": typeof import("./app/routes/api.ausgaben.ts"); + "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"); }; \ No newline at end of file diff --git a/.react-router/types/app/routes/+types/admin.mandanten.ts b/.react-router/types/app/routes/+types/admin.mandanten.ts new file mode 100644 index 0000000..25b52b9 --- /dev/null +++ b/.react-router/types/app/routes/+types/admin.mandanten.ts @@ -0,0 +1,65 @@ +// Generated by React Router + +import type { GetInfo, GetAnnotations } from "react-router/internal"; + +type Module = typeof import("../admin.mandanten.js") + +type Info = GetInfo<{ + file: "routes/admin.mandanten.tsx", + module: Module +}> + +type Matches = [{ + id: "root"; + module: typeof import("../../root.js"); +}, { + id: "routes/admin-layout"; + module: typeof import("../admin-layout.js"); +}, { + id: "routes/admin.mandanten"; + module: typeof import("../admin.mandanten.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.ausgaben.$id.ts b/.react-router/types/app/routes/+types/api.ausgaben.$id.ts new file mode 100644 index 0000000..d95e629 --- /dev/null +++ b/.react-router/types/app/routes/+types/api.ausgaben.$id.ts @@ -0,0 +1,62 @@ +// Generated by React Router + +import type { GetInfo, GetAnnotations } from "react-router/internal"; + +type Module = typeof import("../api.ausgaben.$id.js") + +type Info = GetInfo<{ + file: "routes/api.ausgaben.$id.ts", + module: Module +}> + +type Matches = [{ + id: "root"; + module: typeof import("../../root.js"); +}, { + id: "routes/api.ausgaben.$id"; + module: typeof import("../api.ausgaben.$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.ausgaben.ts b/.react-router/types/app/routes/+types/api.ausgaben.ts new file mode 100644 index 0000000..05eaa42 --- /dev/null +++ b/.react-router/types/app/routes/+types/api.ausgaben.ts @@ -0,0 +1,62 @@ +// Generated by React Router + +import type { GetInfo, GetAnnotations } from "react-router/internal"; + +type Module = typeof import("../api.ausgaben.js") + +type Info = GetInfo<{ + file: "routes/api.ausgaben.ts", + module: Module +}> + +type Matches = [{ + id: "root"; + module: typeof import("../../root.js"); +}, { + id: "routes/api.ausgaben"; + module: typeof import("../api.ausgaben.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.bilanzen.ts b/.react-router/types/app/routes/+types/api.bilanzen.ts new file mode 100644 index 0000000..2d730f1 --- /dev/null +++ b/.react-router/types/app/routes/+types/api.bilanzen.ts @@ -0,0 +1,62 @@ +// Generated by React Router + +import type { GetInfo, GetAnnotations } from "react-router/internal"; + +type Module = typeof import("../api.bilanzen.js") + +type Info = GetInfo<{ + file: "routes/api.bilanzen.ts", + module: Module +}> + +type Matches = [{ + id: "root"; + module: typeof import("../../root.js"); +}, { + id: "routes/api.bilanzen"; + module: typeof import("../api.bilanzen.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.einnahmen.$id.ts b/.react-router/types/app/routes/+types/api.einnahmen.$id.ts new file mode 100644 index 0000000..c2f99bb --- /dev/null +++ b/.react-router/types/app/routes/+types/api.einnahmen.$id.ts @@ -0,0 +1,62 @@ +// Generated by React Router + +import type { GetInfo, GetAnnotations } from "react-router/internal"; + +type Module = typeof import("../api.einnahmen.$id.js") + +type Info = GetInfo<{ + file: "routes/api.einnahmen.$id.ts", + module: Module +}> + +type Matches = [{ + id: "root"; + module: typeof import("../../root.js"); +}, { + id: "routes/api.einnahmen.$id"; + module: typeof import("../api.einnahmen.$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.einnahmen.ts b/.react-router/types/app/routes/+types/api.einnahmen.ts new file mode 100644 index 0000000..a397896 --- /dev/null +++ b/.react-router/types/app/routes/+types/api.einnahmen.ts @@ -0,0 +1,62 @@ +// Generated by React Router + +import type { GetInfo, GetAnnotations } from "react-router/internal"; + +type Module = typeof import("../api.einnahmen.js") + +type Info = GetInfo<{ + file: "routes/api.einnahmen.ts", + module: Module +}> + +type Matches = [{ + id: "root"; + module: typeof import("../../root.js"); +}, { + id: "routes/api.einnahmen"; + module: typeof import("../api.einnahmen.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.ausgaben.ts b/.react-router/types/app/routes/+types/companies.$id.ausgaben.ts new file mode 100644 index 0000000..1a33f1b --- /dev/null +++ b/.react-router/types/app/routes/+types/companies.$id.ausgaben.ts @@ -0,0 +1,65 @@ +// Generated by React Router + +import type { GetInfo, GetAnnotations } from "react-router/internal"; + +type Module = typeof import("../companies.$id.ausgaben.js") + +type Info = GetInfo<{ + file: "routes/companies.$id.ausgaben.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.ausgaben"; + module: typeof import("../companies.$id.ausgaben.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.bilanzen.ts b/.react-router/types/app/routes/+types/companies.$id.bilanzen.ts new file mode 100644 index 0000000..3d0c0bb --- /dev/null +++ b/.react-router/types/app/routes/+types/companies.$id.bilanzen.ts @@ -0,0 +1,65 @@ +// Generated by React Router + +import type { GetInfo, GetAnnotations } from "react-router/internal"; + +type Module = typeof import("../companies.$id.bilanzen.js") + +type Info = GetInfo<{ + file: "routes/companies.$id.bilanzen.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.bilanzen"; + module: typeof import("../companies.$id.bilanzen.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.einnahmen.ts b/.react-router/types/app/routes/+types/companies.$id.einnahmen.ts new file mode 100644 index 0000000..1b56f09 --- /dev/null +++ b/.react-router/types/app/routes/+types/companies.$id.einnahmen.ts @@ -0,0 +1,65 @@ +// Generated by React Router + +import type { GetInfo, GetAnnotations } from "react-router/internal"; + +type Module = typeof import("../companies.$id.einnahmen.js") + +type Info = GetInfo<{ + file: "routes/companies.$id.einnahmen.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.einnahmen"; + module: typeof import("../companies.$id.einnahmen.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/app.css b/app/app.css index 40610c4..25724e7 100644 --- a/app/app.css +++ b/app/app.css @@ -1,5 +1,15 @@ @import "tailwindcss"; +/* Pfeile bei number-inputs ausblenden */ +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} +input[type="number"] { + -moz-appearance: textfield; +} + :root { --background: #f8fafc; --foreground: #0f172a; diff --git a/app/lib/afa.ts b/app/lib/afa.ts new file mode 100644 index 0000000..7dbad21 --- /dev/null +++ b/app/lib/afa.ts @@ -0,0 +1,67 @@ +/** + * Lineare Abschreibung (AfA) nach §7 EStG + * Alle Geldwerte in Euro, 2 Dezimalstellen. + */ + +export interface AnlagegutRaw { + anschaffungskosten: number; + nutzungsdauerJahre: number; + restwert: number; + anschaffungsdatum: string; // ISO-Datumsstring + aktiv: boolean; +} + +/** Volle Jahres-AfA */ +export function jahresAfa(ak: number, restwert: number, nd: number): number { + return Math.round(((ak - restwert) / nd) * 100) / 100; +} + +/** Pro-rata AfA im Anschaffungsjahr: verbleibende Monate (inkl. Anschaffungsmonat) / 12 */ +export function erwerbsjahrAfa(ak: number, restwert: number, nd: number, datum: Date): number { + const verbleibendeMonathe = 12 - datum.getMonth(); // getMonth() = 0-basiert, Jan=0 + return Math.round((jahresAfa(ak, restwert, nd) * verbleibendeMonathe) / 12 * 100) / 100; +} + +/** AfA für ein bestimmtes Kalenderjahr (0 wenn nicht erworben oder vollständig abgeschrieben) */ +export function afaFuerJahr(asset: AnlagegutRaw, year: number): number { + const acqDate = new Date(asset.anschaffungsdatum); + const acqYear = acqDate.getFullYear(); + const lastDepYear = acqYear + asset.nutzungsdauerJahre - 1; + + if (year < acqYear || year > lastDepYear) return 0; + + const ak = asset.anschaffungskosten; + const rv = asset.restwert; + const nd = asset.nutzungsdauerJahre; + + return year === acqYear + ? erwerbsjahrAfa(ak, rv, nd, acqDate) + : jahresAfa(ak, rv, nd); +} + +/** Kumulierte AfA vom Anschaffungsjahr bis inkl. gegebenem Jahr */ +export function kumulierteAfa(asset: AnlagegutRaw, bisJahr: number): number { + const acqYear = new Date(asset.anschaffungsdatum).getFullYear(); + let total = 0; + for (let y = acqYear; y <= bisJahr; y++) { + total += afaFuerJahr(asset, y); + } + return Math.round(total * 100) / 100; +} + +/** Buchwert zum 31.12. des gegebenen Jahres (Minimum: Restwert) */ +export function buchwert(asset: AnlagegutRaw, year: number): number { + const acqYear = new Date(asset.anschaffungsdatum).getFullYear(); + if (year < acqYear) return asset.anschaffungskosten; + const bw = asset.anschaffungskosten - kumulierteAfa(asset, year); + return Math.max(Math.round(bw * 100) / 100, asset.restwert); +} + +/** Anzeige-Status eines Anlageguts */ +export function assetStatus(asset: AnlagegutRaw, currentYear: number): "aktiv" | "vollständig abgeschrieben" | "inaktiv" { + if (!asset.aktiv) return "inaktiv"; + const acqYear = new Date(asset.anschaffungsdatum).getFullYear(); + const lastDepYear = acqYear + asset.nutzungsdauerJahre - 1; + if (currentYear > lastDepYear) return "vollständig abgeschrieben"; + return "aktiv"; +} diff --git a/app/lib/ausgaben.ts b/app/lib/ausgaben.ts new file mode 100644 index 0000000..f23baed --- /dev/null +++ b/app/lib/ausgaben.ts @@ -0,0 +1,41 @@ +export const AUSGABE_KATEGORIEN = [ + "WAREN_ROHSTOFFE", + "GERINGWERTIGE_WIRTSCHAFTSGUETER", + "ABSCHREIBUNGEN", + "MIETE", + "STROM_WASSER", + "TELEKOMMUNIKATION", + "FORTBILDUNG_MESSEN", + "BEITRAEGE", + "VERSICHERUNGEN", + "WERBEKOSTEN", + "ZINSEN", + "REISEKOSTEN", + "REPARATUREN_INSTANDHALTUNG", + "BUEROBEDARF", + "REPRAESENTATIONSKOSTEN", + "SONSTIGER_BETRIEBSBEDARF", + "NEBENKOSTEN_GELDVERKEHR", +] as const; + +export type AusgabeKategorieKey = typeof AUSGABE_KATEGORIEN[number]; + +export const KATEGORIE_LABELS: Record = { + WAREN_ROHSTOFFE: "Waren, Rohstoffe, Hilfsstoffe", + GERINGWERTIGE_WIRTSCHAFTSGUETER: "Geringwertige Wirtschaftsgüter", + ABSCHREIBUNGEN: "Abschreibungen", + MIETE: "Miete", + STROM_WASSER: "Strom, Wasser", + TELEKOMMUNIKATION: "Telekommunikationskosten", + FORTBILDUNG_MESSEN: "Fortbildungskosten/Messen", + BEITRAEGE: "Beiträge", + VERSICHERUNGEN: "Versicherungen", + WERBEKOSTEN: "Werbekosten", + ZINSEN: "Zinsen", + REISEKOSTEN: "Reisekosten", + REPARATUREN_INSTANDHALTUNG: "Reparaturen / Instandhaltung", + BUEROBEDARF: "Bürobedarf", + REPRAESENTATIONSKOSTEN: "Repräsentationskosten", + SONSTIGER_BETRIEBSBEDARF: "Sonstiger Betriebsbedarf", + NEBENKOSTEN_GELDVERKEHR: "Nebenkosten des Geldverkehrs", +}; diff --git a/app/lib/einnahmen.ts b/app/lib/einnahmen.ts new file mode 100644 index 0000000..9a7f069 --- /dev/null +++ b/app/lib/einnahmen.ts @@ -0,0 +1,27 @@ +export const EINNAHME_KATEGORIEN = [ + "FUSSPFLEGE", + "PRIVATEINLAGEN", + "DARLEHEN", + "STEUERERSTATTUNGEN", + "VERSICHERUNGSERSTATTUNGEN", + "ZINSERTRAEGE", + "VERMIETUNG_VERPACHTUNG", + "VERAEUSSERUNGSERLOES", + "EIGENVERBRAUCH", + "SONSTIGE_EINNAHMEN", +] as const; + +export type EinnahmeKategorieKey = typeof EINNAHME_KATEGORIEN[number]; + +export const EINNAHME_LABELS: Record = { + FUSSPFLEGE: "Fußpflege/Verkauf/Gutscheine", + PRIVATEINLAGEN: "Privateinlagen", + DARLEHEN: "Darlehen", + STEUERERSTATTUNGEN: "Steuererstattungen", + VERSICHERUNGSERSTATTUNGEN: "Versicherungserstattungen", + ZINSERTRAEGE: "Zinserträge", + VERMIETUNG_VERPACHTUNG: "Miet-/Pachteinnahmen", + VERAEUSSERUNGSERLOES: "Veräußerungserlöse", + EIGENVERBRAUCH: "Eigenverbrauch", + SONSTIGE_EINNAHMEN: "Sonstige Einnahmen", +}; diff --git a/app/routes.ts b/app/routes.ts index 3f89ac8..0b2a06d 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -17,12 +17,16 @@ export default [ route("companies/:id/invoices/:invoiceId", "routes/companies.$id.invoices.$invoiceId.tsx"), route("companies/:id/invoices/:invoiceId/edit", "routes/companies.$id.invoices.$invoiceId.edit.tsx"), route("companies/:id/reports", "routes/companies.$id.reports.tsx"), + 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("archiv", "routes/archiv.tsx"), route("settings/password", "routes/settings.password.tsx"), ]), // Admin routes layout("routes/admin-layout.tsx", [ + route("admin/mandanten", "routes/admin.mandanten.tsx"), route("admin/users", "routes/admin.users.tsx"), route("admin/users/new", "routes/admin.users.new.tsx"), route("admin/users/:id", "routes/admin.users.$id.tsx"), @@ -43,4 +47,9 @@ export default [ route("api/invoices/:id/pdf", "routes/api.invoices.$id.pdf.ts"), route("api/invoices/:id/xml", "routes/api.invoices.$id.xml.ts"), route("api/reports", "routes/api.reports.ts"), + route("api/bilanzen", "routes/api.bilanzen.ts"), + route("api/ausgaben", "routes/api.ausgaben.ts"), + 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"), ] satisfies RouteConfig; diff --git a/app/routes/admin-layout.tsx b/app/routes/admin-layout.tsx index c4647b7..3875b1b 100644 --- a/app/routes/admin-layout.tsx +++ b/app/routes/admin-layout.tsx @@ -1,6 +1,6 @@ import { Outlet, useLoaderData, Link, useLocation } from "react-router"; import { requireAdmin } from "@/session.server"; -import { Shield, Users, ScrollText, LayoutDashboard } from "lucide-react"; +import { Shield, Users, ScrollText, LayoutDashboard, Building2 } from "lucide-react"; export async function loader({ request }: { request: Request }) { const user = await requireAdmin(request); @@ -12,6 +12,7 @@ export default function AdminLayout() { const location = useLocation(); const navItems = [ + { to: "/admin/mandanten", label: "Mandanten", icon: Building2 }, { to: "/admin/users", label: "Benutzerverwaltung", icon: Users }, { to: "/admin/logs", label: "Audit-Log", icon: ScrollText }, ]; diff --git a/app/routes/admin.mandanten.tsx b/app/routes/admin.mandanten.tsx new file mode 100644 index 0000000..57e985c --- /dev/null +++ b/app/routes/admin.mandanten.tsx @@ -0,0 +1,131 @@ +import { Link, useLoaderData } from "react-router"; +import { requireAdmin } from "@/session.server"; +import prisma from "@/lib/prisma.server"; +import { Badge } from "@/components/ui/badge"; +import { Building2, Archive } from "lucide-react"; + +export async function loader({ request }: { request: Request }) { + await requireAdmin(request); + + const companies = await prisma.company.findMany({ + include: { + user: { select: { id: true, name: true, email: true } }, + _count: { select: { invoices: true, customers: true } }, + }, + orderBy: [{ archived: "asc" }, { name: "asc" }], + }); + + return { + companies: companies.map((c) => ({ + ...c, + archivedAt: c.archivedAt?.toISOString() ?? null, + })), + }; +} + +export default function AdminMandanten() { + const { companies } = useLoaderData(); + + const active = companies.filter((c) => !c.archived); + const archived = companies.filter((c) => c.archived); + + return ( +
+
+

Alle Mandanten

+

+ {companies.length} Mandanten gesamt · {active.length} aktiv · {archived.length} archiviert +

+
+ + + {archived.length > 0 && ( +
+ +
+ )} +
+ ); +} + +type Company = { + id: string; + name: string; + legalForm: string | null; + city: string; + email: string | null; + archived: boolean; + user: { id: string; name: string; email: string }; + _count: { invoices: number; customers: number }; +}; + +function MandantenTabelle({ + companies, + title, + archived = false, +}: { + companies: Company[]; + title: string; + archived?: boolean; +}) { + if (companies.length === 0) return null; + + return ( +
+

+ {title} +

+
+ + + + + + + + + + + + + {companies.map((company) => ( + + + + + + + + + ))} + +
MandantOrtBenutzerRechnungenKunden
+
+ +
+
+ {company.name} + {archived && ( + + )} +
+ {company.legalForm && ( +
{company.legalForm}
+ )} +
+
+
{company.city} +
{company.user.name}
+
{company.user.email}
+
{company._count.invoices}{company._count.customers} + + Öffnen → + +
+
+
+ ); +} diff --git a/app/routes/api.ausgaben.$id.ts b/app/routes/api.ausgaben.$id.ts new file mode 100644 index 0000000..6130f69 --- /dev/null +++ b/app/routes/api.ausgaben.$id.ts @@ -0,0 +1,42 @@ +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), + betrag: z.number().positive(), + datum: z.string().min(1), + beschreibung: z.string().optional(), +}); + +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 ausgabe = await prisma.betriebsausgabe.findFirst({ + where: { id: params.id, company: { userId: user.id } }, + }); + if (!ausgabe) return Response.json({ error: "Not found" }, { status: 404 }); + + if (request.method === "DELETE") { + await prisma.betriebsausgabe.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.betriebsausgabe.update({ + where: { id: params.id }, + data: { + kategorie: parsed.data.kategorie, + betrag: parsed.data.betrag, + datum: new Date(parsed.data.datum), + beschreibung: parsed.data.beschreibung, + }, + }); + + return Response.json({ ...updated, betrag: Number(updated.betrag), datum: updated.datum.toISOString() }); +} diff --git a/app/routes/api.ausgaben.ts b/app/routes/api.ausgaben.ts new file mode 100644 index 0000000..0054ad0 --- /dev/null +++ b/app/routes/api.ausgaben.ts @@ -0,0 +1,73 @@ +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), + betrag: z.number().positive(), + datum: z.string().min(1), + beschreibung: z.string().optional(), +}); + +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 = searchParams.get("year") ? parseInt(searchParams.get("year")!) : null; + + 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 ausgaben = await prisma.betriebsausgabe.findMany({ + where: { + companyId, + ...(year ? { + datum: { + gte: new Date(`${year}-01-01`), + lt: new Date(`${year + 1}-01-01`), + }, + } : {}), + }, + orderBy: { datum: "desc" }, + }); + + return Response.json( + ausgaben.map((a) => ({ + ...a, + betrag: Number(a.betrag), + datum: a.datum.toISOString(), + })) + ); +} + +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 ausgabe = await prisma.betriebsausgabe.create({ + data: { + companyId: parsed.data.companyId, + kategorie: parsed.data.kategorie, + betrag: parsed.data.betrag, + datum: new Date(parsed.data.datum), + beschreibung: parsed.data.beschreibung, + }, + }); + + return Response.json({ ...ausgabe, betrag: Number(ausgabe.betrag), datum: ausgabe.datum.toISOString() }, { status: 201 }); +} diff --git a/app/routes/api.bilanzen.ts b/app/routes/api.bilanzen.ts new file mode 100644 index 0000000..71a4c5f --- /dev/null +++ b/app/routes/api.bilanzen.ts @@ -0,0 +1,119 @@ +import { getApiUser } from "@/session.server"; +import prisma from "@/lib/prisma.server"; +import { InvoiceStatus } from "@prisma/client"; + +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 yearStart = new Date(`${year}-01-01`); + const yearEnd = new Date(`${year + 1}-01-01`); + + // GuV: alle Rechnungen des Jahres (PAID + SENT) + const guvInvoices = await prisma.invoice.findMany({ + where: { + companyId, + status: { in: [InvoiceStatus.PAID, InvoiceStatus.SENT] }, + issueDate: { gte: yearStart, lt: yearEnd }, + }, + include: { items: true }, + }); + + // Umsatzerlöse nach Steuersatz + const erloeseByRate: Record = {}; + for (const invoice of guvInvoices) { + for (const item of invoice.items) { + const rate = String(Number(item.taxRate)); + if (!erloeseByRate[rate]) erloeseByRate[rate] = { netAmount: 0, taxAmount: 0, grossAmount: 0 }; + erloeseByRate[rate].netAmount += Number(item.netAmount); + erloeseByRate[rate].taxAmount += Number(item.taxAmount); + erloeseByRate[rate].grossAmount += Number(item.grossAmount); + } + } + + const guvNetto = guvInvoices.reduce((s, i) => s + Number(i.netTotal), 0); + const guvSteuer = guvInvoices.reduce((s, i) => s + Number(i.taxTotal), 0); + const guvBrutto = guvInvoices.reduce((s, i) => s + Number(i.grossTotal), 0); + + // Bilanz-Stichtag: 31.12. des gewählten Jahres + // Forderungen = offene (SENT) Rechnungen bis Jahresende + const forderungenAgg = await prisma.invoice.aggregate({ + where: { companyId, status: InvoiceStatus.SENT, issueDate: { lt: yearEnd } }, + _sum: { grossTotal: true }, + _count: true, + }); + + // Bank/Kasse-Näherung = bezahlte Rechnungen (brutto) bis Jahresende + const bankAgg = await prisma.invoice.aggregate({ + where: { companyId, status: InvoiceStatus.PAID, issueDate: { lt: yearEnd } }, + _sum: { grossTotal: true }, + _count: true, + }); + + const forderungen = Number(forderungenAgg._sum.grossTotal ?? 0); + const bank = Number(bankAgg._sum.grossTotal ?? 0); + const summeAktiva = forderungen + bank; + + // Betriebsausgaben für das Jahr + const ausgabenAgg = await prisma.betriebsausgabe.aggregate({ + 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 = Number(ausgabenAgg._sum.betrag ?? 0); + + // Sonstige Einnahmen für das Jahr + const einnahmenAgg = await prisma.betriebseinnahme.aggregate({ + where: { companyId, datum: { gte: yearStart, lt: yearEnd } }, + _sum: { betrag: true }, + }); + const sonstigeEinnahmen = Number(einnahmenAgg._sum.betrag ?? 0); + + const jahresergebnis = guvNetto + sonstigeEinnahmen - ausgabenGesamt; + + return Response.json({ + year, + kleinunternehmer: company.kleinunternehmer, + guv: { + erloeseByRate, + netTotal: guvNetto, + taxTotal: guvSteuer, + grossTotal: guvBrutto, + invoiceCount: guvInvoices.length, + ausgabenGesamt, + ausgabenByKategorie: ausgabenByKategorie.map((a) => ({ + kategorie: a.kategorie, + betrag: Number(a._sum.betrag ?? 0), + })), + sonstigeEinnahmen, + jahresergebnis, + }, + bilanz: { + aktiva: { + forderungen: { betrag: forderungen, anzahl: forderungenAgg._count }, + bank: { betrag: bank, anzahl: bankAgg._count }, + summe: summeAktiva, + }, + passiva: { + eigenkapital: summeAktiva, + summe: summeAktiva, + }, + }, + }); +} diff --git a/app/routes/api.einnahmen.$id.ts b/app/routes/api.einnahmen.$id.ts new file mode 100644 index 0000000..3251a88 --- /dev/null +++ b/app/routes/api.einnahmen.$id.ts @@ -0,0 +1,55 @@ +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), + betrag: z.number().positive(), + datum: z.string().min(1), + beschreibung: z.string().optional(), +}); + +/** + * Handles an API request to create, update or delete a einnahme. + * + * @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. + * + * @returns {Promise} - 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 } }, + }); + if (!einnahme) return Response.json({ error: "Not found" }, { status: 404 }); + + if (request.method === "DELETE") { + await prisma.betriebseinnahme.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.betriebseinnahme.update({ + where: { id: params.id }, + data: { + kategorie: parsed.data.kategorie, + betrag: parsed.data.betrag, + datum: new Date(parsed.data.datum), + beschreibung: parsed.data.beschreibung, + }, + }); + + return Response.json({ ...updated, betrag: Number(updated.betrag), datum: updated.datum.toISOString() }); +} diff --git a/app/routes/api.einnahmen.ts b/app/routes/api.einnahmen.ts new file mode 100644 index 0000000..4dc0349 --- /dev/null +++ b/app/routes/api.einnahmen.ts @@ -0,0 +1,105 @@ +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), + betrag: z.number().positive(), + datum: z.string().min(1), + beschreibung: z.string().optional(), +}); + +/** + * Loads the data for the EinnahmenPage. + * + * Requires a companyId search parameter. If year is provided, filters einnahmen for the given year. + * + * Returns a list of einnahmen 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. + * If the company is not found, returns a 404 response with an error message. + */ +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 = searchParams.get("year") ? parseInt(searchParams.get("year")!) : null; + + 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 einnahmen = await prisma.betriebseinnahme.findMany({ + where: { + companyId, + ...(year ? { + datum: { + gte: new Date(`${year}-01-01`), + lt: new Date(`${year + 1}-01-01`), + }, + } : {}), + }, + orderBy: { datum: "asc" }, + }); + + return Response.json( + einnahmen.map((e) => ({ + ...e, + betrag: Number(e.betrag), + datum: e.datum.toISOString(), + })) + ); +} + +/** + * Creates a new einnahme for a given company. + * + * Requires a JSON object in the request body with the following shape: + * { + * companyId: string, + * kategorie: EinnahmeKategorie, + * betrag: number, + * datum: string, + * beschreibung: string, + * } + * + * Returns the created einnahme 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. + * If the company is not found, returns a 404 response with an error message. + */ +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 einnahme = await prisma.betriebseinnahme.create({ + data: { + companyId: parsed.data.companyId, + kategorie: parsed.data.kategorie, + betrag: parsed.data.betrag, + datum: new Date(parsed.data.datum), + beschreibung: parsed.data.beschreibung, + }, + }); + + return Response.json( + { ...einnahme, betrag: Number(einnahme.betrag), datum: einnahme.datum.toISOString() }, + { status: 201 } + ); +} diff --git a/app/routes/companies.$id.anlagevermoegen.tsx b/app/routes/companies.$id.anlagevermoegen.tsx new file mode 100644 index 0000000..51324a7 --- /dev/null +++ b/app/routes/companies.$id.anlagevermoegen.tsx @@ -0,0 +1,366 @@ +import { useState, useEffect } 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"; + +export const handle = { + breadcrumbs: (data: { companyId: string; companyName: string }) => [ + { label: "Mandanten", href: "/companies" }, + { label: data.companyName, href: `/companies/${data.companyId}` }, + { label: "Anlagevermögen" }, + ], +}; + +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; + beschreibung: string | null; + anschaffungsdatum: string; + anschaffungskosten: number; + nutzungsdauerJahre: number; + restwert: number; + aktiv: boolean; + afaJahr: number; + buchwert: number; + status: "aktiv" | "vollständig abgeschrieben" | "inaktiv"; +} + +export async function loader({ request, params }: { request: Request; params: { id: string } }) { + const user = await requireUser(request); + const company = await prisma.company.findFirst({ + where: { id: params.id, userId: user.id }, + 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, + }, + }); + + 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}

} +
+
+
+ +