ADD: changed to rect router

This commit is contained in:
hwinkel
2026-03-10 21:49:01 +01:00
parent 44e79e657f
commit 4bc57b2c4e
102 changed files with 5067 additions and 4824 deletions
+11
View File
@@ -0,0 +1,11 @@
{
"permissions": {
"allow": [
"Bash(npx react-router typegen)",
"Bash(npx react-router build)"
],
"additionalDirectories": [
"/home/henry/.claude/projects/-home-henry-code-AnnasRechnungsManager"
]
}
}
+9
View File
@@ -0,0 +1,9 @@
// Generated by React Router
import "react-router";
declare module "react-router" {
interface Future {
v8_middleware: false
}
}
+233
View File
@@ -0,0 +1,233 @@
// Generated by React Router
import "react-router"
declare module "react-router" {
interface Register {
pages: Pages
routeFiles: RouteFiles
routeModules: RouteModules
}
}
type Pages = {
"/": {
params: {};
};
"/login": {
params: {};
};
"/logout": {
params: {};
};
"/companies": {
params: {};
};
"/companies/new": {
params: {};
};
"/companies/:id": {
params: {
"id": string;
};
};
"/companies/:id/edit": {
params: {
"id": string;
};
};
"/companies/:id/customers": {
params: {
"id": string;
};
};
"/companies/:id/invoices": {
params: {
"id": string;
};
};
"/companies/:id/invoices/new": {
params: {
"id": string;
};
};
"/companies/:id/invoices/:invoiceId": {
params: {
"id": string;
"invoiceId": string;
};
};
"/companies/:id/reports": {
params: {
"id": string;
};
};
"/api/companies": {
params: {};
};
"/api/companies/:id": {
params: {
"id": string;
};
};
"/api/companies/:id/customers": {
params: {
"id": string;
};
};
"/api/companies/:id/invoices": {
params: {
"id": string;
};
};
"/api/customers": {
params: {};
};
"/api/customers/:id": {
params: {
"id": string;
};
};
"/api/invoices": {
params: {};
};
"/api/invoices/:id": {
params: {
"id": string;
};
};
"/api/invoices/:id/pdf": {
params: {
"id": string;
};
};
"/api/reports": {
params: {};
};
};
type RouteFiles = {
"root.tsx": {
id: "root";
page: "/" | "/login" | "/logout" | "/companies" | "/companies/new" | "/companies/:id" | "/companies/:id/edit" | "/companies/:id/customers" | "/companies/:id/invoices" | "/companies/:id/invoices/new" | "/companies/:id/invoices/:invoiceId" | "/companies/:id/reports" | "/api/companies" | "/api/companies/:id" | "/api/companies/:id/customers" | "/api/companies/:id/invoices" | "/api/customers" | "/api/customers/:id" | "/api/invoices" | "/api/invoices/:id" | "/api/invoices/:id/pdf" | "/api/reports";
};
"routes/login.tsx": {
id: "routes/login";
page: "/login";
};
"routes/logout.ts": {
id: "routes/logout";
page: "/logout";
};
"routes/dashboard-layout.tsx": {
id: "routes/dashboard-layout";
page: "/" | "/companies" | "/companies/new" | "/companies/:id" | "/companies/:id/edit" | "/companies/:id/customers" | "/companies/:id/invoices" | "/companies/:id/invoices/new" | "/companies/:id/invoices/:invoiceId" | "/companies/:id/reports";
};
"routes/home.tsx": {
id: "routes/home";
page: "/";
};
"routes/companies.tsx": {
id: "routes/companies";
page: "/companies";
};
"routes/companies.new.tsx": {
id: "routes/companies.new";
page: "/companies/new";
};
"routes/companies.$id.tsx": {
id: "routes/companies.$id";
page: "/companies/:id";
};
"routes/companies.$id.edit.tsx": {
id: "routes/companies.$id.edit";
page: "/companies/:id/edit";
};
"routes/companies.$id.customers.tsx": {
id: "routes/companies.$id.customers";
page: "/companies/:id/customers";
};
"routes/companies.$id.invoices.tsx": {
id: "routes/companies.$id.invoices";
page: "/companies/:id/invoices";
};
"routes/companies.$id.invoices.new.tsx": {
id: "routes/companies.$id.invoices.new";
page: "/companies/:id/invoices/new";
};
"routes/companies.$id.invoices.$invoiceId.tsx": {
id: "routes/companies.$id.invoices.$invoiceId";
page: "/companies/:id/invoices/:invoiceId";
};
"routes/companies.$id.reports.tsx": {
id: "routes/companies.$id.reports";
page: "/companies/:id/reports";
};
"routes/api.companies.ts": {
id: "routes/api.companies";
page: "/api/companies";
};
"routes/api.companies.$id.ts": {
id: "routes/api.companies.$id";
page: "/api/companies/:id";
};
"routes/api.companies.$id.customers.ts": {
id: "routes/api.companies.$id.customers";
page: "/api/companies/:id/customers";
};
"routes/api.companies.$id.invoices.ts": {
id: "routes/api.companies.$id.invoices";
page: "/api/companies/:id/invoices";
};
"routes/api.customers.ts": {
id: "routes/api.customers";
page: "/api/customers";
};
"routes/api.customers.$id.ts": {
id: "routes/api.customers.$id";
page: "/api/customers/:id";
};
"routes/api.invoices.ts": {
id: "routes/api.invoices";
page: "/api/invoices";
};
"routes/api.invoices.$id.ts": {
id: "routes/api.invoices.$id";
page: "/api/invoices/:id";
};
"routes/api.invoices.$id.pdf.ts": {
id: "routes/api.invoices.$id.pdf";
page: "/api/invoices/:id/pdf";
};
"routes/api.reports.ts": {
id: "routes/api.reports";
page: "/api/reports";
};
};
type RouteModules = {
"root": typeof import("./app/root.tsx");
"routes/login": typeof import("./app/routes/login.tsx");
"routes/logout": typeof import("./app/routes/logout.ts");
"routes/dashboard-layout": typeof import("./app/routes/dashboard-layout.tsx");
"routes/home": typeof import("./app/routes/home.tsx");
"routes/companies": typeof import("./app/routes/companies.tsx");
"routes/companies.new": typeof import("./app/routes/companies.new.tsx");
"routes/companies.$id": typeof import("./app/routes/companies.$id.tsx");
"routes/companies.$id.edit": typeof import("./app/routes/companies.$id.edit.tsx");
"routes/companies.$id.customers": typeof import("./app/routes/companies.$id.customers.tsx");
"routes/companies.$id.invoices": typeof import("./app/routes/companies.$id.invoices.tsx");
"routes/companies.$id.invoices.new": typeof import("./app/routes/companies.$id.invoices.new.tsx");
"routes/companies.$id.invoices.$invoiceId": typeof import("./app/routes/companies.$id.invoices.$invoiceId.tsx");
"routes/companies.$id.reports": typeof import("./app/routes/companies.$id.reports.tsx");
"routes/api.companies": typeof import("./app/routes/api.companies.ts");
"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.customers": typeof import("./app/routes/api.customers.ts");
"routes/api.customers.$id": typeof import("./app/routes/api.customers.$id.ts");
"routes/api.invoices": typeof import("./app/routes/api.invoices.ts");
"routes/api.invoices.$id": typeof import("./app/routes/api.invoices.$id.ts");
"routes/api.invoices.$id.pdf": typeof import("./app/routes/api.invoices.$id.pdf.ts");
"routes/api.reports": typeof import("./app/routes/api.reports.ts");
};
+18
View File
@@ -0,0 +1,18 @@
// Generated by React Router
declare module "virtual:react-router/server-build" {
import { ServerBuild } from "react-router";
export const assets: ServerBuild["assets"];
export const assetsBuildDirectory: ServerBuild["assetsBuildDirectory"];
export const basename: ServerBuild["basename"];
export const entry: ServerBuild["entry"];
export const future: ServerBuild["future"];
export const isSpaMode: ServerBuild["isSpaMode"];
export const prerender: ServerBuild["prerender"];
export const publicPath: ServerBuild["publicPath"];
export const routeDiscovery: ServerBuild["routeDiscovery"];
export const routes: ServerBuild["routes"];
export const ssr: ServerBuild["ssr"];
export const allowedActionOrigins: ServerBuild["allowedActionOrigins"];
export const unstable_getCriticalCss: ServerBuild["unstable_getCriticalCss"];
}
+59
View File
@@ -0,0 +1,59 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../root.js")
type Info = GetInfo<{
file: "root.tsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../root.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
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"];
}
@@ -0,0 +1,62 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../api.companies.$id.customers.js")
type Info = GetInfo<{
file: "routes/api.companies.$id.customers.ts",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/api.companies.$id.customers";
module: typeof import("../api.companies.$id.customers.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
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"];
}
@@ -0,0 +1,62 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../api.companies.$id.invoices.js")
type Info = GetInfo<{
file: "routes/api.companies.$id.invoices.ts",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/api.companies.$id.invoices";
module: typeof import("../api.companies.$id.invoices.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
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"];
}
@@ -0,0 +1,62 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../api.companies.$id.js")
type Info = GetInfo<{
file: "routes/api.companies.$id.ts",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/api.companies.$id";
module: typeof import("../api.companies.$id.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
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"];
}
@@ -0,0 +1,62 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../api.companies.js")
type Info = GetInfo<{
file: "routes/api.companies.ts",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/api.companies";
module: typeof import("../api.companies.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
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"];
}
@@ -0,0 +1,62 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../api.customers.$id.js")
type Info = GetInfo<{
file: "routes/api.customers.$id.ts",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/api.customers.$id";
module: typeof import("../api.customers.$id.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
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"];
}
@@ -0,0 +1,62 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../api.customers.js")
type Info = GetInfo<{
file: "routes/api.customers.ts",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/api.customers";
module: typeof import("../api.customers.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
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"];
}
@@ -0,0 +1,62 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../api.invoices.$id.pdf.js")
type Info = GetInfo<{
file: "routes/api.invoices.$id.pdf.ts",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/api.invoices.$id.pdf";
module: typeof import("../api.invoices.$id.pdf.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
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"];
}
@@ -0,0 +1,62 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../api.invoices.$id.js")
type Info = GetInfo<{
file: "routes/api.invoices.$id.ts",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/api.invoices.$id";
module: typeof import("../api.invoices.$id.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
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"];
}
@@ -0,0 +1,62 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../api.invoices.js")
type Info = GetInfo<{
file: "routes/api.invoices.ts",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/api.invoices";
module: typeof import("../api.invoices.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
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"];
}
@@ -0,0 +1,62 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../api.reports.js")
type Info = GetInfo<{
file: "routes/api.reports.ts",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/api.reports";
module: typeof import("../api.reports.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
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"];
}
@@ -0,0 +1,65 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../companies.$id.customers.js")
type Info = GetInfo<{
file: "routes/companies.$id.customers.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.customers";
module: typeof import("../companies.$id.customers.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
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"];
}
@@ -0,0 +1,65 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../companies.$id.edit.js")
type Info = GetInfo<{
file: "routes/companies.$id.edit.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.edit";
module: typeof import("../companies.$id.edit.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
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"];
}
@@ -0,0 +1,65 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../companies.$id.invoices.$invoiceId.js")
type Info = GetInfo<{
file: "routes/companies.$id.invoices.$invoiceId.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.invoices.$invoiceId";
module: typeof import("../companies.$id.invoices.$invoiceId.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
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"];
}
@@ -0,0 +1,65 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../companies.$id.invoices.new.js")
type Info = GetInfo<{
file: "routes/companies.$id.invoices.new.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.invoices.new";
module: typeof import("../companies.$id.invoices.new.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
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"];
}
@@ -0,0 +1,65 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../companies.$id.invoices.js")
type Info = GetInfo<{
file: "routes/companies.$id.invoices.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.invoices";
module: typeof import("../companies.$id.invoices.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
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"];
}
@@ -0,0 +1,65 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../companies.$id.reports.js")
type Info = GetInfo<{
file: "routes/companies.$id.reports.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.reports";
module: typeof import("../companies.$id.reports.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
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"];
}
@@ -0,0 +1,65 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../companies.$id.js")
type Info = GetInfo<{
file: "routes/companies.$id.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";
module: typeof import("../companies.$id.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
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"];
}
@@ -0,0 +1,65 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../companies.new.js")
type Info = GetInfo<{
file: "routes/companies.new.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.new";
module: typeof import("../companies.new.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
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"];
}
@@ -0,0 +1,65 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../companies.js")
type Info = GetInfo<{
file: "routes/companies.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";
module: typeof import("../companies.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
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"];
}
@@ -0,0 +1,62 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../dashboard-layout.js")
type Info = GetInfo<{
file: "routes/dashboard-layout.tsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/dashboard-layout";
module: typeof import("../dashboard-layout.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
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"];
}
@@ -0,0 +1,65 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../home.js")
type Info = GetInfo<{
file: "routes/home.tsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/dashboard-layout";
module: typeof import("../dashboard-layout.js");
}, {
id: "routes/home";
module: typeof import("../home.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
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"];
}
@@ -0,0 +1,62 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../login.js")
type Info = GetInfo<{
file: "routes/login.tsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/login";
module: typeof import("../login.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
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"];
}
@@ -0,0 +1,62 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../logout.js")
type Info = GetInfo<{
file: "routes/logout.ts",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/logout";
module: typeof import("../logout.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
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"];
}
+12 -9
View File
@@ -11,18 +11,19 @@ Buchhaltungs- und Rechnungsverwaltungssystem für Mandanten.
## Tech Stack
- **Next.js 14** (App Router) + TypeScript
- **MySQL / MariaDB** via Prisma ORM
- **NextAuth.js v5** (Email/Passwort-Login)
- **Tailwind CSS** + shadcn/ui
- **React Router v7** (Framework Mode, SSR) + TypeScript
- **MariaDB / MySQL** via Prisma ORM
- **Cookie-Session-Auth** (bcryptjs, kein NextAuth)
- **Tailwind CSS v4** + shadcn/ui
- **@react-pdf/renderer** für PDF-Generierung
- **Docker** für die Datenbank
## Setup
### 1. Voraussetzungen
- Node.js 18+
- MySQL oder MariaDB
- Node.js 20+
- Docker (für MariaDB)
### 2. Installation
@@ -30,11 +31,11 @@ Buchhaltungs- und Rechnungsverwaltungssystem für Mandanten.
npm install
```
### 3. Datenbank konfigurieren
### 3. Umgebungsvariablen konfigurieren
```bash
cp .env.example .env
# DATABASE_URL in .env anpassen
# DATABASE_URL und AUTH_SECRET in .env anpassen
```
### 4. Datenbank einrichten
@@ -52,7 +53,9 @@ npx prisma db seed
npm run dev
```
Öffne [http://localhost:3000](http://localhost:3000)
Startet Docker (PostgreSQL) und den Vite-Dev-Server.
Öffne [http://localhost:5173](http://localhost:5173)
## Datenbank-Kommandos
View File
@@ -1,5 +1,3 @@
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
@@ -1,5 +1,3 @@
"use client";
import { useState, useCallback } from "react";
import { useForm, useFieldArray } from "react-hook-form";
import { Button } from "@/components/ui/button";
@@ -128,7 +126,6 @@ export function InvoiceForm({ customers, companyId, onSubmit }: InvoiceFormProps
return (
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-6">
{/* Header info */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label>Kunde *</Label>
@@ -161,7 +158,6 @@ export function InvoiceForm({ customers, companyId, onSubmit }: InvoiceFormProps
</div>
</div>
{/* Items */}
<div>
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide">Rechnungspositionen</h3>
@@ -176,7 +172,6 @@ export function InvoiceForm({ customers, companyId, onSubmit }: InvoiceFormProps
</div>
<div className="border border-gray-200 rounded-xl overflow-hidden">
{/* Header */}
<div className="grid grid-cols-12 gap-2 px-3 py-2 bg-gray-50 border-b border-gray-200 text-xs font-medium text-gray-600">
<div className="col-span-4">Beschreibung</div>
<div className="col-span-1">Menge</div>
@@ -194,9 +189,6 @@ export function InvoiceForm({ customers, companyId, onSubmit }: InvoiceFormProps
{...register(`items.${index}.description`, { required: true })}
placeholder="Leistungsbeschreibung"
className="text-sm"
onChange={(e) => {
register(`items.${index}.description`).onChange(e);
}}
/>
</div>
<div className="col-span-1">
@@ -260,7 +252,6 @@ export function InvoiceForm({ customers, companyId, onSubmit }: InvoiceFormProps
))}
</div>
{/* Totals */}
<div className="mt-4 flex justify-end">
<div className="w-64 space-y-1.5">
<div className="flex justify-between text-sm text-gray-600">
@@ -279,7 +270,6 @@ export function InvoiceForm({ customers, companyId, onSubmit }: InvoiceFormProps
</div>
</div>
{/* Notes */}
<div className="space-y-1.5">
<Label>Anmerkungen</Label>
<Textarea {...register("notes")} placeholder="Zahlungsbedingungen, Hinweise..." rows={3} />
@@ -246,7 +246,6 @@ interface InvoicePDFProps {
export function InvoicePDFDocument({ invoice }: InvoicePDFProps) {
const n = (v: unknown) => Number(v);
// Group items by tax rate
const taxGroups = invoice.items.reduce(
(acc, item) => {
const rate = n(item.taxRate);
@@ -261,7 +260,6 @@ export function InvoicePDFDocument({ invoice }: InvoicePDFProps) {
return (
<Document>
<Page size="A4" style={styles.page}>
{/* Header */}
<View style={styles.header}>
<View>
<Text style={styles.companyName}>{invoice.company.name}</Text>
@@ -287,10 +285,8 @@ export function InvoicePDFDocument({ invoice }: InvoicePDFProps) {
</View>
</View>
{/* Invoice Title */}
<Text style={styles.invoiceTitle}>Rechnung</Text>
{/* Address Section */}
<View style={styles.addressSection}>
<View style={styles.addressBlock}>
<Text style={styles.addressLabel}>Rechnungsempfänger</Text>
@@ -308,7 +304,6 @@ export function InvoicePDFDocument({ invoice }: InvoicePDFProps) {
</View>
</View>
{/* Meta */}
<View style={styles.metaGrid}>
<View style={styles.metaItem}>
<Text style={styles.metaLabel}>Rechnungsnummer</Text>
@@ -330,7 +325,6 @@ export function InvoicePDFDocument({ invoice }: InvoicePDFProps) {
</View>
</View>
{/* Items Table */}
<View style={styles.tableHeader}>
<Text style={{ ...styles.tableHeaderText, ...styles.col_pos }}>#</Text>
<Text style={{ ...styles.tableHeaderText, ...styles.col_desc }}>Beschreibung</Text>
@@ -355,7 +349,6 @@ export function InvoicePDFDocument({ invoice }: InvoicePDFProps) {
</View>
))}
{/* Totals */}
<View style={styles.totalsSection}>
<View style={styles.totalsTable}>
<View style={styles.totalsRow}>
@@ -375,7 +368,6 @@ export function InvoicePDFDocument({ invoice }: InvoicePDFProps) {
</View>
</View>
{/* Notes */}
{invoice.notes && (
<View style={styles.notes}>
<Text style={styles.notesLabel}>Hinweise</Text>
@@ -383,7 +375,6 @@ export function InvoicePDFDocument({ invoice }: InvoicePDFProps) {
</View>
)}
{/* Footer */}
<View style={styles.footer} fixed>
<Text style={styles.footerText}>
{invoice.company.name}
@@ -1,12 +1,7 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { signOut } from "next-auth/react";
import { Link, Form, useLocation } from "react-router";
import {
Calculator,
Building2,
FileText,
LayoutDashboard,
LogOut,
ChevronRight,
@@ -26,11 +21,11 @@ const navItems: NavItem[] = [
];
export function Sidebar({ userName }: { userName?: string | null }) {
const pathname = usePathname();
const location = useLocation();
const pathname = location.pathname;
return (
<aside className="w-60 shrink-0 flex flex-col bg-white border-r border-gray-200 min-h-screen">
{/* Logo */}
<div className="flex items-center gap-3 px-4 py-5 border-b border-gray-200">
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-indigo-600">
<Calculator className="w-4 h-4 text-white" />
@@ -40,14 +35,13 @@ export function Sidebar({ userName }: { userName?: string | null }) {
</span>
</div>
{/* Nav */}
<nav className="flex-1 px-3 py-4 space-y-0.5">
{navItems.map((item) => {
const active = pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href));
return (
<Link
key={item.href}
href={item.href}
to={item.href}
className={cn(
"flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors",
active
@@ -63,20 +57,21 @@ export function Sidebar({ userName }: { userName?: string | null }) {
})}
</nav>
{/* User */}
<div className="px-3 py-4 border-t border-gray-200">
{userName && (
<p className="text-xs text-gray-500 px-3 mb-2 truncate">{userName}</p>
)}
<Button
variant="ghost"
size="sm"
className="w-full justify-start text-gray-600 hover:text-red-600 hover:bg-red-50"
onClick={() => signOut({ callbackUrl: "/login" })}
>
<LogOut className="h-4 w-4" />
Abmelden
</Button>
<Form method="post" action="/logout">
<Button
type="submit"
variant="ghost"
size="sm"
className="w-full justify-start text-gray-600 hover:text-red-600 hover:bg-red-50"
>
<LogOut className="h-4 w-4" />
Abmelden
</Button>
</Form>
</div>
</aside>
);
@@ -1,5 +1,3 @@
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
@@ -1,5 +1,3 @@
"use client";
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown } from "lucide-react";
@@ -3,7 +3,6 @@ import prisma from "./prisma";
export async function generateInvoiceNumber(companyId: string): Promise<string> {
const year = new Date().getFullYear();
// Atomically increment the sequence number
const company = await prisma.company.update({
where: { id: companyId },
data: { invoiceSequence: { increment: 1 } },
View File
+25
View File
@@ -0,0 +1,25 @@
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
import "./app.css";
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="de">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Annas Rechnungsmanager</title>
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}
+31
View File
@@ -0,0 +1,31 @@
import { type RouteConfig, index, route, layout } from "@react-router/dev/routes";
export default [
route("login", "routes/login.tsx"),
route("logout", "routes/logout.ts"),
layout("routes/dashboard-layout.tsx", [
index("routes/home.tsx"),
route("companies", "routes/companies.tsx"),
route("companies/new", "routes/companies.new.tsx"),
route("companies/:id", "routes/companies.$id.tsx"),
route("companies/:id/edit", "routes/companies.$id.edit.tsx"),
route("companies/:id/customers", "routes/companies.$id.customers.tsx"),
route("companies/:id/invoices", "routes/companies.$id.invoices.tsx"),
route("companies/:id/invoices/new", "routes/companies.$id.invoices.new.tsx"),
route("companies/:id/invoices/:invoiceId", "routes/companies.$id.invoices.$invoiceId.tsx"),
route("companies/:id/reports", "routes/companies.$id.reports.tsx"),
]),
// API resource routes
route("api/companies", "routes/api.companies.ts"),
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/customers", "routes/api.customers.ts"),
route("api/customers/:id", "routes/api.customers.$id.ts"),
route("api/invoices", "routes/api.invoices.ts"),
route("api/invoices/:id", "routes/api.invoices.$id.ts"),
route("api/invoices/:id/pdf", "routes/api.invoices.$id.pdf.ts"),
route("api/reports", "routes/api.reports.ts"),
] satisfies RouteConfig;
+17
View File
@@ -0,0 +1,17 @@
import { getApiUser } from "@/session.server";
import prisma from "@/lib/prisma";
export async function loader({ request, params }: { request: Request; params: { id: string } }) {
const user = await getApiUser(request);
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
const company = await prisma.company.findFirst({ where: { id: params.id, userId: user.id } });
if (!company) return Response.json({ error: "Not found" }, { status: 404 });
const customers = await prisma.customer.findMany({
where: { companyId: params.id },
orderBy: { name: "asc" },
});
return Response.json(customers);
}
+18
View File
@@ -0,0 +1,18 @@
import { getApiUser } from "@/session.server";
import prisma from "@/lib/prisma";
export async function loader({ request, params }: { request: Request; params: { id: string } }) {
const user = await getApiUser(request);
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
const company = await prisma.company.findFirst({ where: { id: params.id, userId: user.id } });
if (!company) return Response.json({ error: "Not found" }, { status: 404 });
const invoices = await prisma.invoice.findMany({
where: { companyId: params.id },
include: { customer: { select: { name: true } } },
orderBy: { issueDate: "desc" },
});
return Response.json(invoices);
}
+52
View File
@@ -0,0 +1,52 @@
import { getApiUser } from "@/session.server";
import prisma from "@/lib/prisma";
import { z } from "zod";
const companySchema = z.object({
name: z.string().min(1),
legalForm: z.string().optional(),
taxId: z.string().optional(),
vatId: z.string().optional(),
address: z.string().min(1),
zip: z.string().min(1),
city: z.string().min(1),
country: z.string().optional(),
email: z.string().email().optional().or(z.literal("")),
phone: z.string().optional(),
website: z.string().optional(),
bankIban: z.string().optional(),
bankBic: z.string().optional(),
bankName: z.string().optional(),
invoicePrefix: z.string().optional(),
});
export async function loader({ request, params }: { request: Request; params: { id: string } }) {
const user = await getApiUser(request);
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
const company = await prisma.company.findFirst({ where: { id: params.id, userId: user.id } });
if (!company) return Response.json({ error: "Not found" }, { status: 404 });
return Response.json(company);
}
export async function action({ request, params }: { request: Request; params: { id: string } }) {
const user = await getApiUser(request);
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
const company = await prisma.company.findFirst({ where: { id: params.id, userId: user.id } });
if (!company) return Response.json({ error: "Not found" }, { status: 404 });
if (request.method === "DELETE") {
await prisma.company.delete({ where: { id: params.id } });
return Response.json({ ok: true });
}
// PUT
const body = await request.json();
const parsed = companySchema.safeParse(body);
if (!parsed.success) return Response.json({ error: parsed.error.issues }, { status: 400 });
const updated = await prisma.company.update({ where: { id: params.id }, data: parsed.data });
return Response.json(updated);
}
@@ -1,5 +1,4 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { getApiUser } from "@/session.server";
import prisma from "@/lib/prisma";
import { z } from "zod";
@@ -21,32 +20,32 @@ const companySchema = z.object({
invoicePrefix: z.string().optional().default("RE"),
});
export async function GET() {
const session = await auth();
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
export async function loader({ request }: { request: Request }) {
const user = await getApiUser(request);
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
const companies = await prisma.company.findMany({
where: { userId: session.user.id },
where: { userId: user.id },
include: { _count: { select: { invoices: true, customers: true } } },
orderBy: { name: "asc" },
});
return NextResponse.json(companies);
return Response.json(companies);
}
export async function POST(req: NextRequest) {
const session = await auth();
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
export async function action({ request }: { request: Request }) {
const user = await getApiUser(request);
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
const body = await req.json();
const body = await request.json();
const parsed = companySchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: parsed.error.issues }, { status: 400 });
return Response.json({ error: parsed.error.issues }, { status: 400 });
}
const company = await prisma.company.create({
data: { ...parsed.data, userId: session.user.id },
data: { ...parsed.data, userId: user.id },
});
return NextResponse.json(company, { status: 201 });
return Response.json(company, { status: 201 });
}
+50
View File
@@ -0,0 +1,50 @@
import { getApiUser } from "@/session.server";
import prisma from "@/lib/prisma";
import { z } from "zod";
const customerSchema = z.object({
name: z.string().min(1),
vatId: z.string().optional(),
taxId: z.string().optional(),
address: z.string().min(1),
zip: z.string().min(1),
city: z.string().min(1),
country: z.string().optional(),
email: z.string().email().optional().or(z.literal("")),
phone: z.string().optional(),
});
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 customer = await prisma.customer.findFirst({
where: { id: params.id, company: { userId: user.id } },
});
if (!customer) return Response.json({ error: "Not found" }, { status: 404 });
return Response.json(customer);
}
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 customer = await prisma.customer.findFirst({
where: { id: params.id, company: { userId: user.id } },
});
if (!customer) return Response.json({ error: "Not found" }, { status: 404 });
if (request.method === "DELETE") {
await prisma.customer.delete({ where: { id: params.id } });
return Response.json({ ok: true });
}
// PUT
const body = await request.json();
const parsed = customerSchema.safeParse(body);
if (!parsed.success) return Response.json({ error: parsed.error.issues }, { status: 400 });
const updated = await prisma.customer.update({ where: { id: params.id }, data: parsed.data });
return Response.json(updated);
}
+33
View File
@@ -0,0 +1,33 @@
import { getApiUser } from "@/session.server";
import prisma from "@/lib/prisma";
import { z } from "zod";
const customerSchema = z.object({
companyId: z.string().min(1),
name: z.string().min(1),
vatId: z.string().optional(),
taxId: z.string().optional(),
address: z.string().min(1),
zip: z.string().min(1),
city: z.string().min(1),
country: z.string().optional().default("DE"),
email: z.string().email().optional().or(z.literal("")),
phone: z.string().optional(),
});
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 = customerSchema.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 customer = await prisma.customer.create({ data: parsed.data });
return Response.json(customer, { status: 201 });
}
@@ -1,15 +1,12 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { getApiUser } from "@/session.server";
import prisma from "@/lib/prisma";
export async function GET(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await auth();
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { id } = await params;
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 invoice = await prisma.invoice.findFirst({
where: { id, company: { userId: session.user.id } },
where: { id: params.id, company: { userId: user.id } },
include: {
items: { orderBy: { position: "asc" } },
customer: true,
@@ -17,9 +14,8 @@ export async function GET(_req: NextRequest, { params }: { params: Promise<{ id:
},
});
if (!invoice) return NextResponse.json({ error: "Not found" }, { status: 404 });
if (!invoice) return Response.json({ error: "Not found" }, { status: 404 });
// Dynamic import to avoid SSR bundling issues with @react-pdf/renderer
const { renderToBuffer } = await import("@react-pdf/renderer");
const React = (await import("react")).default;
const { InvoicePDFDocument } = await import("@/components/invoice/invoice-pdf");
@@ -28,7 +24,7 @@ export async function GET(_req: NextRequest, { params }: { params: Promise<{ id:
const element = React.createElement(InvoicePDFDocument as any, { invoice }) as any;
const buffer = await renderToBuffer(element);
return new NextResponse(new Uint8Array(buffer), {
return new Response(new Uint8Array(buffer), {
status: 200,
headers: {
"Content-Type": "application/pdf",
+47
View File
@@ -0,0 +1,47 @@
import { getApiUser } from "@/session.server";
import prisma from "@/lib/prisma";
import { InvoiceStatus } from "@prisma/client";
import { z } from "zod";
async function getInvoice(id: string, userId: string) {
return prisma.invoice.findFirst({
where: { id, company: { userId } },
include: { items: { orderBy: { position: "asc" } }, customer: true, company: true },
});
}
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 invoice = await getInvoice(params.id, user.id);
if (!invoice) return Response.json({ error: "Not found" }, { status: 404 });
return Response.json(invoice);
}
const statusSchema = z.object({ status: z.nativeEnum(InvoiceStatus) });
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 invoice = await getInvoice(params.id, user.id);
if (!invoice) return Response.json({ error: "Not found" }, { status: 404 });
if (request.method === "DELETE") {
await prisma.invoice.delete({ where: { id: params.id } });
return Response.json({ ok: true });
}
// PATCH
const body = await request.json();
const parsed = statusSchema.safeParse(body);
if (!parsed.success) return Response.json({ error: parsed.error.issues }, { status: 400 });
const updated = await prisma.invoice.update({
where: { id: params.id },
data: { status: parsed.data.status },
});
return Response.json(updated);
}
@@ -1,5 +1,4 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { getApiUser } from "@/session.server";
import prisma from "@/lib/prisma";
import { generateInvoiceNumber } from "@/lib/invoice-number";
import { z } from "zod";
@@ -29,21 +28,18 @@ const invoiceSchema = z.object({
grossTotal: z.number(),
});
export async function POST(req: NextRequest) {
const session = await auth();
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
export async function action({ request }: { request: Request }) {
const user = await getApiUser(request);
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
const body = await req.json();
const body = await request.json();
const parsed = invoiceSchema.safeParse(body);
if (!parsed.success) return NextResponse.json({ error: parsed.error.issues }, { status: 400 });
if (!parsed.success) return Response.json({ error: parsed.error.issues }, { status: 400 });
const { items, companyId, ...invoiceData } = parsed.data;
// Verify company belongs to user
const company = await prisma.company.findFirst({
where: { id: companyId, userId: session.user.id },
});
if (!company) return NextResponse.json({ error: "Company not found" }, { status: 404 });
const company = await prisma.company.findFirst({ where: { id: companyId, userId: user.id } });
if (!company) return Response.json({ error: "Company not found" }, { status: 404 });
const number = await generateInvoiceNumber(companyId);
@@ -55,20 +51,10 @@ export async function POST(req: NextRequest) {
issueDate: new Date(invoiceData.issueDate),
deliveryDate: invoiceData.deliveryDate ? new Date(invoiceData.deliveryDate) : null,
dueDate: new Date(invoiceData.dueDate),
items: {
create: items.map((item) => ({
...item,
quantity: item.quantity,
unitPrice: item.unitPrice,
taxRate: item.taxRate,
netAmount: item.netAmount,
taxAmount: item.taxAmount,
grossAmount: item.grossAmount,
})),
},
items: { create: items },
},
include: { items: true, customer: true },
});
return NextResponse.json(invoice, { status: 201 });
return Response.json(invoice, { status: 201 });
}
@@ -1,24 +1,20 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { getApiUser } from "@/session.server";
import prisma from "@/lib/prisma";
import { InvoiceStatus } from "@prisma/client";
export async function GET(req: NextRequest) {
const session = await auth();
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
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(req.url);
const { searchParams } = new URL(request.url);
const companyId = searchParams.get("companyId");
const year = parseInt(searchParams.get("year") ?? String(new Date().getFullYear()));
if (!companyId) return NextResponse.json({ error: "companyId required" }, { status: 400 });
if (!companyId) return Response.json({ error: "companyId required" }, { status: 400 });
const company = await prisma.company.findFirst({
where: { id: companyId, userId: session.user.id },
});
if (!company) return NextResponse.json({ error: "Not found" }, { status: 404 });
const company = await prisma.company.findFirst({ where: { id: companyId, userId: user.id } });
if (!company) return Response.json({ error: "Not found" }, { status: 404 });
// Get all paid/sent invoices for the year
const invoices = await prisma.invoice.findMany({
where: {
companyId,
@@ -28,14 +24,10 @@ export async function GET(req: NextRequest) {
lt: new Date(`${year + 1}-01-01`),
},
},
include: {
items: true,
customer: { select: { name: true } },
},
include: { items: true, customer: { select: { name: true } } },
orderBy: { issueDate: "asc" },
});
// Build monthly summary
const monthly: Record<number, {
month: number;
invoiceCount: number;
@@ -65,7 +57,6 @@ export async function GET(req: NextRequest) {
}
}
// Quarterly
const quarterly = [1, 2, 3, 4].map((q) => {
const months = [q * 3 - 2, q * 3 - 1, q * 3];
const data = months.map((m) => monthly[m]);
@@ -97,5 +88,5 @@ export async function GET(req: NextRequest) {
grossTotal: invoices.reduce((s, i) => s + Number(i.grossTotal), 0),
};
return NextResponse.json({ year, monthly: Object.values(monthly), quarterly, yearTotal, invoices });
return Response.json({ year, monthly: Object.values(monthly), quarterly, yearTotal, invoices });
}
@@ -1,8 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import Link from "next/link";
import { useState } from "react";
import { Link, useLoaderData, useParams, useRevalidator } from "react-router";
import { requireUser } from "@/session.server";
import prisma from "@/lib/prisma";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
@@ -28,23 +27,27 @@ type FormData = z.infer<typeof schema>;
interface Customer {
id: string;
name: string;
vatId?: string | null | undefined;
vatId?: string | null;
address: string;
zip: string;
city: string;
email?: string | null | undefined;
phone?: string | null | undefined;
email?: string | null;
phone?: string | null;
}
interface CustomerForEdit {
id: string;
name: string;
vatId?: string | undefined;
address: string;
zip: string;
city: string;
email?: string | undefined;
phone?: string | undefined;
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 },
});
if (!company) throw new Response("Not Found", { status: 404 });
const customers = await prisma.customer.findMany({
where: { companyId: params.id },
orderBy: { name: "asc" },
});
return { customers, companyId: params.id };
}
function CustomerForm({
@@ -103,17 +106,10 @@ function CustomerForm({
}
export default function CustomersPage() {
const { id: companyId } = useParams<{ id: string }>();
const [customers, setCustomers] = useState<Customer[]>([]);
const { customers, companyId } = useLoaderData<typeof loader>();
const { revalidate } = useRevalidator();
const [open, setOpen] = useState(false);
const [editCustomer, setEditCustomer] = useState<CustomerForEdit | null>(null);
async function load() {
const res = await fetch(`/api/companies/${companyId}/customers`);
if (res.ok) setCustomers(await res.json());
}
useEffect(() => { load(); }, [companyId]);
const [editCustomer, setEditCustomer] = useState<Customer | null>(null);
async function handleCreate(data: FormData) {
await fetch("/api/customers", {
@@ -122,7 +118,7 @@ export default function CustomersPage() {
body: JSON.stringify({ ...data, companyId }),
});
setOpen(false);
load();
revalidate();
}
async function handleEdit(data: FormData) {
@@ -133,19 +129,19 @@ export default function CustomersPage() {
body: JSON.stringify(data),
});
setEditCustomer(null);
load();
revalidate();
}
async function handleDelete(customerId: string) {
if (!confirm("Kunden wirklich löschen?")) return;
await fetch(`/api/customers/${customerId}`, { method: "DELETE" });
load();
revalidate();
}
return (
<div>
<Link
href={`/companies/${companyId}`}
to={`/companies/${companyId}`}
className="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700 mb-6"
>
<ChevronLeft className="h-4 w-4" /> Zurück zum Mandanten
@@ -169,7 +165,6 @@ export default function CustomersPage() {
</Dialog>
</div>
{/* Edit Dialog */}
<Dialog open={!!editCustomer} onOpenChange={(o) => !o && setEditCustomer(null)}>
<DialogContent>
<DialogHeader>
@@ -177,7 +172,15 @@ export default function CustomersPage() {
</DialogHeader>
{editCustomer && (
<CustomerForm
defaultValues={editCustomer}
defaultValues={{
name: editCustomer.name,
address: editCustomer.address,
zip: editCustomer.zip,
city: editCustomer.city,
vatId: editCustomer.vatId ?? undefined,
email: editCustomer.email ?? undefined,
phone: editCustomer.phone ?? undefined,
}}
onSubmit={handleEdit}
submitLabel="Speichern"
/>
@@ -219,16 +222,7 @@ export default function CustomersPage() {
<Button
variant="ghost"
size="icon"
onClick={() => setEditCustomer({
id: customer.id,
name: customer.name,
address: customer.address,
zip: customer.zip,
city: customer.city,
vatId: customer.vatId ?? undefined,
email: customer.email ?? undefined,
phone: customer.phone ?? undefined,
})}
onClick={() => setEditCustomer(customer)}
>
<Edit className="h-4 w-4" />
</Button>
@@ -1,36 +1,36 @@
"use client";
import { useEffect, useState } from "react";
import { useRouter, useParams } from "next/navigation";
import { Link, useLoaderData, useNavigate } from "react-router";
import { requireUser } from "@/session.server";
import prisma from "@/lib/prisma";
import { CompanyForm } from "@/components/company/company-form";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import Link from "next/link";
import { ChevronLeft } from "lucide-react";
export default function EditCompanyPage() {
const router = useRouter();
const { id } = useParams<{ id: string }>();
const [company, setCompany] = useState<Record<string, unknown> | null>(null);
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 },
});
if (!company) throw new Response("Not Found", { status: 404 });
return { company };
}
useEffect(() => {
fetch(`/api/companies/${id}`).then((r) => r.json()).then(setCompany);
}, [id]);
export default function EditCompanyPage() {
const { company } = useLoaderData<typeof loader>();
const navigate = useNavigate();
async function handleSubmit(data: Record<string, unknown>) {
const res = await fetch(`/api/companies/${id}`, {
const res = await fetch(`/api/companies/${company.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (res.ok) router.push(`/companies/${id}`);
if (res.ok) navigate(`/companies/${company.id}`);
}
if (!company) return <div className="p-8 text-gray-500">Lade...</div>;
return (
<div>
<Link
href={`/companies/${id}`}
to={`/companies/${company.id}`}
className="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700 mb-6"
>
<ChevronLeft className="h-4 w-4" /> Zurück zum Mandanten
@@ -1,23 +1,26 @@
import { auth } from "@/lib/auth";
import { Link, useLoaderData, useNavigate, useRevalidator } from "react-router";
import { requireUser } from "@/session.server";
import prisma from "@/lib/prisma";
import { notFound } from "next/navigation";
import Link from "next/link";
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { InvoiceStatusBadge } from "@/components/invoice/invoice-status-badge";
import { InvoiceActions } from "./invoice-actions";
import { formatCurrency, formatDate } from "@/lib/tax";
import { ChevronLeft } from "lucide-react";
import { ChevronLeft, Download, CheckCircle, Send, Trash2 } from "lucide-react";
import { InvoiceStatus } from "@prisma/client";
export default async function InvoiceDetailPage({
export async function loader({
request,
params,
}: {
params: Promise<{ id: string; invoiceId: string }>;
request: Request;
params: { id: string; invoiceId: string };
}) {
const { id, invoiceId } = await params;
const session = await auth();
const user = await requireUser(request);
const { id, invoiceId } = params;
const invoice = await prisma.invoice.findFirst({
where: { id: invoiceId, companyId: id, company: { userId: session!.user!.id! } },
where: { id: invoiceId, companyId: id, company: { userId: user.id } },
include: {
items: { orderBy: { position: "asc" } },
customer: true,
@@ -25,24 +28,81 @@ export default async function InvoiceDetailPage({
},
});
if (!invoice) notFound();
if (!invoice) throw new Response("Not Found", { status: 404 });
return {
invoice: {
...invoice,
netTotal: Number(invoice.netTotal),
taxTotal: Number(invoice.taxTotal),
grossTotal: Number(invoice.grossTotal),
issueDate: invoice.issueDate.toISOString(),
dueDate: invoice.dueDate.toISOString(),
deliveryDate: invoice.deliveryDate?.toISOString() ?? null,
items: invoice.items.map((item) => ({
...item,
quantity: Number(item.quantity),
unitPrice: Number(item.unitPrice),
taxRate: Number(item.taxRate),
netAmount: Number(item.netAmount),
taxAmount: Number(item.taxAmount),
grossAmount: Number(item.grossAmount),
})),
},
};
}
export default function InvoiceDetailPage() {
const { invoice } = useLoaderData<typeof loader>();
const navigate = useNavigate();
const { revalidate } = useRevalidator();
const [loading, setLoading] = useState(false);
const id = invoice.companyId;
// Group items by tax rate for totals block
const taxGroups = invoice.items.reduce(
(acc, item) => {
const rate = Number(item.taxRate);
const rate = item.taxRate;
if (!acc[rate]) acc[rate] = { net: 0, tax: 0 };
acc[rate].net += Number(item.netAmount);
acc[rate].tax += Number(item.taxAmount);
acc[rate].net += item.netAmount;
acc[rate].tax += item.taxAmount;
return acc;
},
{} as Record<number, { net: number; tax: number }>
);
async function updateStatus(status: InvoiceStatus) {
setLoading(true);
await fetch(`/api/invoices/${invoice.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status }),
});
setLoading(false);
revalidate();
}
async function handleDelete() {
if (!confirm("Rechnung wirklich löschen?")) return;
await fetch(`/api/invoices/${invoice.id}`, { method: "DELETE" });
navigate(`/companies/${id}/invoices`);
}
async function downloadPdf() {
const res = await fetch(`/api/invoices/${invoice.id}/pdf`);
if (!res.ok) return;
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `rechnung-${invoice.number}.pdf`;
a.click();
URL.revokeObjectURL(url);
}
return (
<div>
<Link
href={`/companies/${id}/invoices`}
to={`/companies/${id}/invoices`}
className="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700 mb-6"
>
<ChevronLeft className="h-4 w-4" /> Zurück zu Rechnungen
@@ -58,15 +118,44 @@ export default async function InvoiceDetailPage({
{invoice.customer.name} · {formatDate(invoice.issueDate)}
</p>
</div>
<InvoiceActions invoice={{ id: invoice.id, status: invoice.status, companyId: id }} />
{/* Invoice Actions */}
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={downloadPdf}>
<Download className="h-4 w-4" /> PDF
</Button>
{invoice.status === "DRAFT" && (
<Button size="sm" onClick={() => updateStatus(InvoiceStatus.SENT)} disabled={loading}>
<Send className="h-4 w-4" /> Als versendet markieren
</Button>
)}
{invoice.status === "SENT" && (
<Button
size="sm"
className="bg-green-600 hover:bg-green-700"
onClick={() => updateStatus(InvoiceStatus.PAID)}
disabled={loading}
>
<CheckCircle className="h-4 w-4" /> Als bezahlt markieren
</Button>
)}
{(invoice.status === "DRAFT" || invoice.status === "CANCELLED") && (
<Button
variant="outline"
size="sm"
className="text-red-600 hover:text-red-700 hover:bg-red-50"
onClick={handleDelete}
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
{/* Invoice document preview */}
<Card>
<CardContent className="p-6">
{/* Sender & Recipient */}
<div className="grid grid-cols-2 gap-8 mb-8">
<div>
<p className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">Absender</p>
@@ -92,7 +181,6 @@ export default async function InvoiceDetailPage({
</div>
</div>
{/* Dates */}
<div className="grid grid-cols-3 gap-4 mb-8 p-4 bg-gray-50 rounded-lg">
<div>
<p className="text-xs text-gray-500">Rechnungsnummer</p>
@@ -114,7 +202,6 @@ export default async function InvoiceDetailPage({
</div>
</div>
{/* Items table */}
<div className="border border-gray-200 rounded-lg overflow-hidden mb-6">
<table className="w-full text-sm">
<thead>
@@ -133,23 +220,22 @@ export default async function InvoiceDetailPage({
<td className="px-4 py-3 text-gray-500">{item.position}</td>
<td className="px-4 py-3 text-gray-900">{item.description}</td>
<td className="px-4 py-3 text-right text-gray-700">
{Number(item.quantity)} {item.unit && <span className="text-gray-500">{item.unit}</span>}
{item.quantity} {item.unit && <span className="text-gray-500">{item.unit}</span>}
</td>
<td className="px-4 py-3 text-right text-gray-700">{formatCurrency(Number(item.unitPrice))}</td>
<td className="px-4 py-3 text-right text-gray-700">{Number(item.taxRate)}%</td>
<td className="px-4 py-3 text-right font-medium text-gray-900">{formatCurrency(Number(item.grossAmount))}</td>
<td className="px-4 py-3 text-right text-gray-700">{formatCurrency(item.unitPrice)}</td>
<td className="px-4 py-3 text-right text-gray-700">{item.taxRate}%</td>
<td className="px-4 py-3 text-right font-medium text-gray-900">{formatCurrency(item.grossAmount)}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Totals */}
<div className="flex justify-end">
<div className="w-72 space-y-1.5">
<div className="flex justify-between text-sm text-gray-600">
<span>Nettobetrag</span>
<span>{formatCurrency(Number(invoice.netTotal))}</span>
<span>{formatCurrency(invoice.netTotal)}</span>
</div>
{Object.entries(taxGroups).map(([rate, { net, tax }]) => (
<div key={rate} className="flex justify-between text-sm text-gray-600">
@@ -159,7 +245,7 @@ export default async function InvoiceDetailPage({
))}
<div className="flex justify-between text-base font-bold text-gray-900 border-t border-gray-300 pt-2">
<span>Gesamtbetrag (brutto)</span>
<span>{formatCurrency(Number(invoice.grossTotal))}</span>
<span>{formatCurrency(invoice.grossTotal)}</span>
</div>
</div>
</div>
@@ -171,7 +257,6 @@ export default async function InvoiceDetailPage({
</div>
)}
{/* Bank details */}
{invoice.company.bankIban && (
<div className="mt-4 pt-4 border-t border-gray-100">
<p className="text-xs text-gray-500">
@@ -185,7 +270,6 @@ export default async function InvoiceDetailPage({
</Card>
</div>
{/* Sidebar */}
<div className="space-y-4">
<Card>
<CardHeader>
@@ -194,15 +278,15 @@ export default async function InvoiceDetailPage({
<CardContent className="space-y-3">
<div>
<p className="text-xs text-gray-500">Netto</p>
<p className="font-medium text-gray-900">{formatCurrency(Number(invoice.netTotal))}</p>
<p className="font-medium text-gray-900">{formatCurrency(invoice.netTotal)}</p>
</div>
<div>
<p className="text-xs text-gray-500">MwSt.</p>
<p className="font-medium text-gray-900">{formatCurrency(Number(invoice.taxTotal))}</p>
<p className="font-medium text-gray-900">{formatCurrency(invoice.taxTotal)}</p>
</div>
<div className="border-t border-gray-200 pt-2">
<p className="text-xs text-gray-500">Brutto</p>
<p className="text-lg font-bold text-gray-900">{formatCurrency(Number(invoice.grossTotal))}</p>
<p className="text-lg font-bold text-gray-900">{formatCurrency(invoice.grossTotal)}</p>
</div>
</CardContent>
</Card>
+73
View File
@@ -0,0 +1,73 @@
import { Link, useLoaderData, useNavigate, redirect } from "react-router";
import { requireUser } from "@/session.server";
import prisma from "@/lib/prisma";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { InvoiceForm } from "@/components/invoice/invoice-form";
import { ChevronLeft } from "lucide-react";
export async function loader({ request, params }: { request: Request; params: { id: string } }) {
const user = await requireUser(request);
const { id } = params;
const company = await prisma.company.findFirst({
where: { id, userId: user.id },
});
if (!company) throw new Response("Not Found", { status: 404 });
const customers = await prisma.customer.findMany({
where: { companyId: id },
orderBy: { name: "asc" },
select: { id: true, name: true },
});
if (customers.length === 0) {
throw redirect(`/companies/${id}/customers`);
}
return { company, customers };
}
export default function NewInvoicePage() {
const { company, customers } = useLoaderData<typeof loader>();
const navigate = useNavigate();
async function handleSubmit(data: Record<string, unknown>) {
const res = await fetch("/api/invoices", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (res.ok) {
const invoice = await res.json();
navigate(`/companies/${company.id}/invoices/${invoice.id}`);
} else {
alert("Fehler beim Erstellen der Rechnung.");
}
}
return (
<div>
<Link
to={`/companies/${company.id}/invoices`}
className="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700 mb-6"
>
<ChevronLeft className="h-4 w-4" /> Zurück zu Rechnungen
</Link>
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Neue Rechnung</h1>
<p className="text-gray-500 mt-1">Für Mandant: {company.name}</p>
</div>
<Card>
<CardHeader>
<CardTitle>Rechnungsdaten</CardTitle>
</CardHeader>
<CardContent>
<InvoiceForm customers={customers} companyId={company.id} onSubmit={handleSubmit} />
</CardContent>
</Card>
</div>
);
}
@@ -1,21 +1,20 @@
import { auth } from "@/lib/auth";
import { Link, useLoaderData } from "react-router";
import { requireUser } from "@/session.server";
import prisma from "@/lib/prisma";
import { notFound } from "next/navigation";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { InvoiceStatusBadge } from "@/components/invoice/invoice-status-badge";
import { formatCurrency, formatDate } from "@/lib/tax";
import { Plus, FileText, ChevronLeft } from "lucide-react";
export default async function InvoicesPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const session = await auth();
export async function loader({ request, params }: { request: Request; params: { id: string } }) {
const user = await requireUser(request);
const { id } = params;
const company = await prisma.company.findFirst({
where: { id, userId: session!.user!.id! },
where: { id, userId: user.id },
});
if (!company) notFound();
if (!company) throw new Response("Not Found", { status: 404 });
const invoices = await prisma.invoice.findMany({
where: { companyId: id },
@@ -23,10 +22,25 @@ export default async function InvoicesPage({ params }: { params: Promise<{ id: s
orderBy: { issueDate: "desc" },
});
return {
company,
invoices: invoices.map((inv) => ({
...inv,
grossTotal: Number(inv.grossTotal),
issueDate: inv.issueDate.toISOString(),
dueDate: inv.dueDate.toISOString(),
})),
};
}
export default function InvoicesPage() {
const { company, invoices } = useLoaderData<typeof loader>();
const id = company.id;
return (
<div>
<Link
href={`/companies/${id}`}
to={`/companies/${id}`}
className="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700 mb-6"
>
<ChevronLeft className="h-4 w-4" /> {company.name}
@@ -38,7 +52,7 @@ export default async function InvoicesPage({ params }: { params: Promise<{ id: s
<p className="text-gray-500 mt-1">{invoices.length} Rechnungen für {company.name}</p>
</div>
<Button asChild>
<Link href={`/companies/${id}/invoices/new`}>
<Link to={`/companies/${id}/invoices/new`}>
<Plus className="h-4 w-4" /> Neue Rechnung
</Link>
</Button>
@@ -51,7 +65,7 @@ export default async function InvoicesPage({ params }: { params: Promise<{ id: s
<h3 className="font-semibold text-gray-700 mb-2">Noch keine Rechnungen</h3>
<p className="text-gray-500 mb-6 text-sm">Erstellen Sie die erste Rechnung für diesen Mandanten.</p>
<Button asChild>
<Link href={`/companies/${id}/invoices/new`}>
<Link to={`/companies/${id}/invoices/new`}>
<Plus className="h-4 w-4" /> Erste Rechnung erstellen
</Link>
</Button>
@@ -63,7 +77,7 @@ export default async function InvoicesPage({ params }: { params: Promise<{ id: s
{invoices.map((invoice) => (
<Link
key={invoice.id}
href={`/companies/${id}/invoices/${invoice.id}`}
to={`/companies/${id}/invoices/${invoice.id}`}
className="flex items-center justify-between px-6 py-4 hover:bg-gray-50 transition-colors group"
>
<div className="flex items-center gap-4">
@@ -79,7 +93,7 @@ export default async function InvoicesPage({ params }: { params: Promise<{ id: s
<p className="text-sm text-gray-500">{formatDate(invoice.issueDate)}</p>
<InvoiceStatusBadge status={invoice.status} />
<p className="font-medium text-gray-900 w-28 text-right">
{formatCurrency(Number(invoice.grossTotal))}
{formatCurrency(invoice.grossTotal)}
</p>
</div>
</Link>
@@ -1,8 +1,5 @@
"use client";
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import Link from "next/link";
import { useState, useEffect } from "react";
import { Link, useParams } from "react-router";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { formatCurrency } from "@/lib/tax";
import { ChevronLeft, TrendingUp, BarChart3 } from "lucide-react";
@@ -62,7 +59,7 @@ export default function ReportsPage() {
return (
<div>
<Link
href={`/companies/${companyId}`}
to={`/companies/${companyId}`}
className="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700 mb-6"
>
<ChevronLeft className="h-4 w-4" /> Zurück zum Mandanten
@@ -86,7 +83,6 @@ export default function ReportsPage() {
<div className="text-center text-gray-500 py-12">Lade Auswertung...</div>
) : data && (
<div className="space-y-6">
{/* Year Summary */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card>
<CardContent className="pt-6">
@@ -114,7 +110,6 @@ export default function ReportsPage() {
</Card>
</div>
{/* Quarterly UStVA */}
<Card>
<CardHeader>
<div className="flex items-center gap-2">
@@ -173,7 +168,6 @@ export default function ReportsPage() {
</CardContent>
</Card>
{/* Monthly breakdown */}
<Card>
<CardHeader>
<div className="flex items-center gap-2">
@@ -1,7 +1,6 @@
import { auth } from "@/lib/auth";
import { Link, useLoaderData } from "react-router";
import { requireUser } from "@/session.server";
import prisma from "@/lib/prisma";
import { notFound } from "next/navigation";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
@@ -26,12 +25,12 @@ const statusVariants: Record<InvoiceStatus, "secondary" | "default" | "success"
CANCELLED: "destructive",
};
export default async function CompanyPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const session = await auth();
export async function loader({ request, params }: { request: Request; params: { id: string } }) {
const user = await requireUser(request);
const { id } = params;
const company = await prisma.company.findFirst({
where: { id, userId: session!.user!.id! },
where: { id, userId: user.id },
include: {
invoices: {
include: { customer: { select: { name: true } } },
@@ -42,16 +41,33 @@ export default async function CompanyPage({ params }: { params: Promise<{ id: st
},
});
if (!company) notFound();
if (!company) throw new Response("Not Found", { status: 404 });
const revenue = await prisma.invoice.aggregate({
where: { companyId: id, status: InvoiceStatus.PAID },
_sum: { grossTotal: true },
});
return {
company: {
...company,
invoices: company.invoices.map((inv) => ({
...inv,
grossTotal: Number(inv.grossTotal),
issueDate: inv.issueDate.toISOString(),
dueDate: inv.dueDate.toISOString(),
})),
},
revenue: Number(revenue._sum.grossTotal ?? 0),
};
}
export default function CompanyPage() {
const { company, revenue } = useLoaderData<typeof loader>();
const id = company.id;
return (
<div>
{/* Header */}
<div className="flex items-start justify-between mb-8">
<div className="flex items-start gap-4">
<div className="p-3 rounded-xl bg-indigo-50">
@@ -66,16 +82,15 @@ export default async function CompanyPage({ params }: { params: Promise<{ id: st
</div>
</div>
<Button variant="outline" asChild>
<Link href={`/companies/${id}/edit`}>
<Link to={`/companies/${id}/edit`}>
<Edit className="h-4 w-4" />
Bearbeiten
</Link>
</Button>
</div>
{/* Quick Actions */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-8">
<Link href={`/companies/${id}/invoices/new`} className="block">
<Link to={`/companies/${id}/invoices/new`} className="block">
<Card className="hover:border-indigo-200 hover:shadow-sm transition-all cursor-pointer">
<CardContent className="pt-4 pb-4 flex items-center gap-3">
<div className="p-2 rounded-lg bg-indigo-50">
@@ -85,7 +100,7 @@ export default async function CompanyPage({ params }: { params: Promise<{ id: st
</CardContent>
</Card>
</Link>
<Link href={`/companies/${id}/invoices`} className="block">
<Link to={`/companies/${id}/invoices`} className="block">
<Card className="hover:border-blue-200 hover:shadow-sm transition-all cursor-pointer">
<CardContent className="pt-4 pb-4 flex items-center gap-3">
<div className="p-2 rounded-lg bg-blue-50">
@@ -95,7 +110,7 @@ export default async function CompanyPage({ params }: { params: Promise<{ id: st
</CardContent>
</Card>
</Link>
<Link href={`/companies/${id}/customers`} className="block">
<Link to={`/companies/${id}/customers`} className="block">
<Card className="hover:border-green-200 hover:shadow-sm transition-all cursor-pointer">
<CardContent className="pt-4 pb-4 flex items-center gap-3">
<div className="p-2 rounded-lg bg-green-50">
@@ -105,7 +120,7 @@ export default async function CompanyPage({ params }: { params: Promise<{ id: st
</CardContent>
</Card>
</Link>
<Link href={`/companies/${id}/reports`} className="block">
<Link to={`/companies/${id}/reports`} className="block">
<Card className="hover:border-purple-200 hover:shadow-sm transition-all cursor-pointer">
<CardContent className="pt-4 pb-4 flex items-center gap-3">
<div className="p-2 rounded-lg bg-purple-50">
@@ -118,11 +133,10 @@ export default async function CompanyPage({ params }: { params: Promise<{ id: st
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Recent Invoices */}
<div className="lg:col-span-2">
<div className="flex items-center justify-between mb-4">
<h2 className="font-semibold text-gray-900">Letzte Rechnungen</h2>
<Link href={`/companies/${id}/invoices`} className="text-sm text-indigo-600 hover:text-indigo-700">
<Link to={`/companies/${id}/invoices`} className="text-sm text-indigo-600 hover:text-indigo-700">
Alle anzeigen
</Link>
</div>
@@ -137,7 +151,7 @@ export default async function CompanyPage({ params }: { params: Promise<{ id: st
{company.invoices.map((invoice) => (
<Link
key={invoice.id}
href={`/companies/${id}/invoices/${invoice.id}`}
to={`/companies/${id}/invoices/${invoice.id}`}
className="flex items-center justify-between px-4 py-3 hover:bg-gray-50 transition-colors"
>
<div>
@@ -147,7 +161,7 @@ export default async function CompanyPage({ params }: { params: Promise<{ id: st
<div className="flex items-center gap-3">
<Badge variant={statusVariants[invoice.status]}>{statusLabels[invoice.status]}</Badge>
<span className="text-sm font-medium text-gray-900">
{formatCurrency(Number(invoice.grossTotal))}
{formatCurrency(invoice.grossTotal)}
</span>
</div>
</Link>
@@ -157,7 +171,6 @@ export default async function CompanyPage({ params }: { params: Promise<{ id: st
</Card>
</div>
{/* Company Info */}
<div className="space-y-4">
<Card>
<CardHeader>
@@ -166,7 +179,7 @@ export default async function CompanyPage({ params }: { params: Promise<{ id: st
<CardContent className="space-y-3">
<div>
<p className="text-xs text-gray-500">Bezahlt (gesamt)</p>
<p className="font-semibold text-gray-900">{formatCurrency(Number(revenue._sum.grossTotal ?? 0))}</p>
<p className="font-semibold text-gray-900">{formatCurrency(revenue)}</p>
</div>
<div>
<p className="text-xs text-gray-500">Rechnungen</p>
@@ -1,13 +1,10 @@
"use client";
import { useRouter } from "next/navigation";
import { Link, useNavigate } from "react-router";
import { CompanyForm } from "@/components/company/company-form";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import Link from "next/link";
import { ChevronLeft } from "lucide-react";
export default function NewCompanyPage() {
const router = useRouter();
const navigate = useNavigate();
async function handleSubmit(data: Record<string, unknown>) {
const res = await fetch("/api/companies", {
@@ -18,14 +15,14 @@ export default function NewCompanyPage() {
if (res.ok) {
const company = await res.json();
router.push(`/companies/${company.id}`);
navigate(`/companies/${company.id}`);
}
}
return (
<div>
<Link
href="/companies"
to="/companies"
className="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700 mb-6"
>
<ChevronLeft className="h-4 w-4" /> Zurück zu Mandanten
@@ -1,17 +1,22 @@
import { auth } from "@/lib/auth";
import { Link, useLoaderData } from "react-router";
import { requireUser } from "@/session.server";
import prisma from "@/lib/prisma";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Building2, Plus, FileText, Users } from "lucide-react";
export default async function CompaniesPage() {
const session = await auth();
export async function loader({ request }: { request: Request }) {
const user = await requireUser(request);
const companies = await prisma.company.findMany({
where: { userId: session!.user!.id! },
where: { userId: user.id },
include: { _count: { select: { invoices: true, customers: true } } },
orderBy: { name: "asc" },
});
return { companies };
}
export default function CompaniesPage() {
const { companies } = useLoaderData<typeof loader>();
return (
<div>
@@ -21,7 +26,7 @@ export default async function CompaniesPage() {
<p className="text-gray-500 mt-1">{companies.length} Mandanten verwaltet</p>
</div>
<Button asChild>
<Link href="/companies/new">
<Link to="/companies/new">
<Plus className="h-4 w-4" />
Mandant anlegen
</Link>
@@ -35,7 +40,7 @@ export default async function CompaniesPage() {
<h3 className="font-semibold text-gray-700 mb-2">Noch keine Mandanten</h3>
<p className="text-gray-500 mb-6 text-sm">Legen Sie Ihren ersten Mandanten an, um loszulegen.</p>
<Button asChild>
<Link href="/companies/new">
<Link to="/companies/new">
<Plus className="h-4 w-4" />
Ersten Mandanten anlegen
</Link>
@@ -45,7 +50,7 @@ export default async function CompaniesPage() {
) : (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
{companies.map((company) => (
<Link key={company.id} href={`/companies/${company.id}`}>
<Link key={company.id} to={`/companies/${company.id}`}>
<Card className="hover:shadow-md transition-all hover:border-indigo-200 cursor-pointer h-full">
<CardHeader>
<div className="flex items-start gap-3">
+23
View File
@@ -0,0 +1,23 @@
import { Outlet, useLoaderData } from "react-router";
import { requireUser } from "@/session.server";
import { Sidebar } from "@/components/layout/sidebar";
export async function loader({ request }: { request: Request }) {
const user = await requireUser(request);
return { userName: user.name };
}
export default function DashboardLayout() {
const { userName } = useLoaderData<typeof loader>();
return (
<div className="flex min-h-screen bg-gray-50">
<Sidebar userName={userName} />
<main className="flex-1 overflow-auto">
<div className="max-w-7xl mx-auto px-6 py-8">
<Outlet />
</div>
</main>
</div>
);
}
@@ -1,16 +1,16 @@
import { auth } from "@/lib/auth";
import { Link, useLoaderData } from "react-router";
import { requireUser } from "@/session.server";
import prisma from "@/lib/prisma";
import { formatCurrency } from "@/lib/tax";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Building2, FileText, Euro, TrendingUp } from "lucide-react";
import Link from "next/link";
import { Building2, FileText, Euro } from "lucide-react";
import { InvoiceStatus } from "@prisma/client";
export default async function DashboardPage() {
const session = await auth();
const userId = session!.user!.id!;
export async function loader({ request }: { request: Request }) {
const user = await requireUser(request);
const userId = user.id;
const [companies, invoiceStats] = await Promise.all([
const [companies, invoiceStats, paidTotal, openInvoices] = await Promise.all([
prisma.company.findMany({
where: { userId },
include: { _count: { select: { invoices: true, customers: true } } },
@@ -21,16 +21,25 @@ export default async function DashboardPage() {
_count: true,
_sum: { grossTotal: true },
}),
prisma.invoice.aggregate({
where: { company: { userId }, status: InvoiceStatus.PAID },
_sum: { grossTotal: true },
}),
prisma.invoice.count({
where: { company: { userId }, status: { in: [InvoiceStatus.SENT, InvoiceStatus.DRAFT] } },
}),
]);
const paidTotal = await prisma.invoice.aggregate({
where: { company: { userId }, status: InvoiceStatus.PAID },
_sum: { grossTotal: true },
});
return {
companies,
totalInvoices: invoiceStats._count,
paidTotal: Number(paidTotal._sum.grossTotal ?? 0),
openInvoices,
};
}
const openInvoices = await prisma.invoice.count({
where: { company: { userId }, status: { in: [InvoiceStatus.SENT, InvoiceStatus.DRAFT] } },
});
export default function DashboardPage() {
const { companies, totalInvoices, paidTotal, openInvoices } = useLoaderData<typeof loader>();
return (
<div>
@@ -39,7 +48,6 @@ export default async function DashboardPage() {
<p className="text-gray-500 mt-1">Übersicht aller Mandanten und Rechnungen</p>
</div>
{/* Stats */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<Card>
<CardContent className="pt-6">
@@ -62,7 +70,7 @@ export default async function DashboardPage() {
<FileText className="h-5 w-5 text-blue-600" />
</div>
<div>
<p className="text-2xl font-bold text-gray-900">{invoiceStats._count}</p>
<p className="text-2xl font-bold text-gray-900">{totalInvoices}</p>
<p className="text-sm text-gray-500">Rechnungen gesamt</p>
</div>
</div>
@@ -90,9 +98,7 @@ export default async function DashboardPage() {
<Euro className="h-5 w-5 text-green-600" />
</div>
<div>
<p className="text-2xl font-bold text-gray-900">
{formatCurrency(Number(paidTotal._sum.grossTotal ?? 0))}
</p>
<p className="text-2xl font-bold text-gray-900">{formatCurrency(paidTotal)}</p>
<p className="text-sm text-gray-500">Bezahlt (brutto)</p>
</div>
</div>
@@ -100,14 +106,10 @@ export default async function DashboardPage() {
</Card>
</div>
{/* Companies */}
<div>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900">Mandanten</h2>
<Link
href="/companies"
className="text-sm text-indigo-600 hover:text-indigo-700 font-medium"
>
<Link to="/companies" className="text-sm text-indigo-600 hover:text-indigo-700 font-medium">
Alle anzeigen
</Link>
</div>
@@ -118,7 +120,7 @@ export default async function DashboardPage() {
<Building2 className="h-12 w-12 text-gray-300 mx-auto mb-4" />
<p className="text-gray-500 mb-4">Noch keine Mandanten angelegt.</p>
<Link
href="/companies/new"
to="/companies/new"
className="inline-flex items-center gap-2 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 transition-colors"
>
Mandant anlegen
@@ -128,7 +130,7 @@ export default async function DashboardPage() {
) : (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
{companies.map((company) => (
<Link key={company.id} href={`/companies/${company.id}`}>
<Link key={company.id} to={`/companies/${company.id}`}>
<Card className="hover:shadow-md transition-shadow cursor-pointer">
<CardHeader>
<CardTitle className="text-base">{company.name}</CardTitle>
@@ -1,41 +1,32 @@
"use client";
import { useState } from "react";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
import { Form, useActionData, useNavigation, redirect } from "react-router";
import { login, createUserSession, getUserSession } from "@/session.server";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Calculator, AlertCircle } from "lucide-react";
export async function loader({ request }: { request: Request }) {
const { userId } = await getUserSession(request);
if (userId) throw redirect("/");
return null;
}
export async function action({ request }: { request: Request }) {
const formData = await request.formData();
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const user = await login(email, password);
if (!user) return { error: "E-Mail oder Passwort falsch." };
return createUserSession(user.id, user.name, "/");
}
export default function LoginPage() {
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
setError("");
const res = await signIn("credentials", {
email,
password,
redirect: false,
});
setLoading(false);
if (res?.error) {
setError("E-Mail oder Passwort falsch.");
} else {
router.push("/");
router.refresh();
}
}
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
const loading = navigation.state === "submitting";
return (
<div className="min-h-screen bg-gradient-to-br from-indigo-50 to-blue-50 flex items-center justify-center p-4">
@@ -54,11 +45,11 @@ export default function LoginPage() {
<CardDescription>Geben Sie Ihre Zugangsdaten ein</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<Form method="post" className="space-y-4">
{actionData?.error && (
<div className="flex items-center gap-2 rounded-lg bg-red-50 border border-red-200 p-3 text-sm text-red-700">
<AlertCircle className="h-4 w-4 shrink-0" />
{error}
{actionData.error}
</div>
)}
@@ -66,10 +57,9 @@ export default function LoginPage() {
<Label htmlFor="email">E-Mail</Label>
<Input
id="email"
name="email"
type="email"
placeholder="anna@example.de"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
autoComplete="email"
/>
@@ -79,9 +69,8 @@ export default function LoginPage() {
<Label htmlFor="password">Passwort</Label>
<Input
id="password"
name="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
autoComplete="current-password"
/>
@@ -90,7 +79,7 @@ export default function LoginPage() {
<Button type="submit" className="w-full" disabled={loading}>
{loading ? "Anmelden..." : "Anmelden"}
</Button>
</form>
</Form>
</CardContent>
</Card>
</div>
+5
View File
@@ -0,0 +1,5 @@
import { logout } from "@/session.server";
export async function action({ request }: { request: Request }) {
return logout(request);
}
+66
View File
@@ -0,0 +1,66 @@
import { createCookieSessionStorage, redirect } from "react-router";
import bcrypt from "bcryptjs";
import prisma from "@/lib/prisma";
const sessionStorage = createCookieSessionStorage({
cookie: {
name: "__session",
httpOnly: true,
maxAge: 60 * 60 * 24 * 30,
path: "/",
sameSite: "lax",
secrets: [process.env.AUTH_SECRET ?? "fallback-secret-change-in-production"],
secure: process.env.NODE_ENV === "production",
},
});
export async function login(email: string, password: string) {
const user = await prisma.user.findUnique({ where: { email } });
if (!user) return null;
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) return null;
return { id: user.id, email: user.email, name: user.name };
}
export async function createUserSession(
userId: string,
userName: string,
redirectTo: string
) {
const session = await sessionStorage.getSession();
session.set("userId", userId);
session.set("userName", userName);
return redirect(redirectTo, {
headers: { "Set-Cookie": await sessionStorage.commitSession(session) },
});
}
export async function getUserSession(request: Request) {
const session = await sessionStorage.getSession(
request.headers.get("Cookie")
);
return {
userId: session.get("userId") as string | undefined,
userName: session.get("userName") as string | undefined,
};
}
export async function requireUser(request: Request) {
const { userId, userName } = await getUserSession(request);
if (!userId) throw redirect("/login");
return { id: userId, name: userName as string | undefined };
}
export async function getApiUser(request: Request) {
const { userId } = await getUserSession(request);
return userId ? { id: userId } : null;
}
export async function logout(request: Request) {
const session = await sessionStorage.getSession(
request.headers.get("Cookie")
);
return redirect("/login", {
headers: { "Set-Cookie": await sessionStorage.destroySession(session) },
});
}
+47
View File
@@ -0,0 +1,47 @@
services:
mariadb:
image: mariadb:11
container_name: annas_mariadb
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: annas_rechnungen
MYSQL_USER: annas_user
MYSQL_PASSWORD: annas_password
volumes:
- mariadb_data:/var/lib/mysql
ports:
- "3306:3306"
networks:
- db_network
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
start_period: 10s
interval: 5s
timeout: 5s
retries: 5
phpmyadmin:
image: phpmyadmin:latest
container_name: annas_phpmyadmin
restart: unless-stopped
environment:
PMA_HOST: mariadb
PMA_PORT: 3306
PMA_USER: root
PMA_PASSWORD: rootpassword
UPLOAD_LIMIT: 100M
ports:
- "8080:80"
depends_on:
mariadb:
condition: service_healthy
networks:
- db_network
volumes:
mariadb_data:
networks:
db_network:
driver: bridge
+12 -17
View File
@@ -1,18 +1,13 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
import js from "@eslint/js";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;
export default [
js.configs.recommended,
{
ignores: ["build/**", ".react-router/**", "node_modules/**"],
},
{
rules: {
"no-unused-vars": "off",
},
},
];
-7
View File
@@ -1,7 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;
+2253 -3863
View File
File diff suppressed because it is too large Load Diff
+16 -10
View File
@@ -2,10 +2,12 @@
"name": "annas-app",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"dev": "docker-compose up -d && react-router dev",
"build": "react-router build",
"start": "react-router-serve ./build/server/index.js",
"typecheck": "react-router typegen && tsc",
"lint": "eslint",
"db:migrate": "prisma migrate dev",
"db:seed": "ts-node --compiler-options '{\"module\":\"CommonJS\"}' prisma/seed.ts",
@@ -15,7 +17,6 @@
"seed": "npx ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
},
"dependencies": {
"@auth/prisma-adapter": "^2.11.1",
"@hookform/resolvers": "^5.2.2",
"@prisma/client": "^5.22.0",
"@radix-ui/react-dialog": "^1.1.15",
@@ -26,29 +27,34 @@
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-toast": "^1.2.15",
"@react-pdf/renderer": "^4.3.2",
"@react-router/node": "^7.13.1",
"@react-router/serve": "^7",
"@types/bcryptjs": "^2.4.6",
"bcryptjs": "^3.0.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"isbot": "^5",
"lucide-react": "^0.577.0",
"next": "16.1.6",
"next-auth": "^5.0.0-beta.30",
"prisma": "^5.22.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"react": "^19",
"react-dom": "^19",
"react-hook-form": "^7.71.2",
"react-router": "^7",
"tailwind-merge": "^3.5.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@react-router/dev": "^7.13.1",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^4",
"eslint": "^9",
"eslint-config-next": "16.1.6",
"tailwindcss": "^4",
"typescript": "^5"
"typescript": "^5",
"vite": "^6",
"vite-tsconfig-paths": "^5"
}
}
-7
View File
@@ -1,7 +0,0 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
@@ -0,0 +1,110 @@
-- CreateTable
CREATE TABLE `users` (
`id` VARCHAR(191) NOT NULL,
`email` VARCHAR(191) NOT NULL,
`passwordHash` VARCHAR(191) NOT NULL,
`name` VARCHAR(191) NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
UNIQUE INDEX `users_email_key`(`email`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `companies` (
`id` VARCHAR(191) NOT NULL,
`name` VARCHAR(191) NOT NULL,
`legalForm` VARCHAR(191) NULL,
`taxId` VARCHAR(191) NULL,
`vatId` VARCHAR(191) NULL,
`address` VARCHAR(191) NOT NULL,
`zip` VARCHAR(191) NOT NULL,
`city` VARCHAR(191) NOT NULL,
`country` VARCHAR(191) NOT NULL DEFAULT 'DE',
`email` VARCHAR(191) NULL,
`phone` VARCHAR(191) NULL,
`website` VARCHAR(191) NULL,
`bankIban` VARCHAR(191) NULL,
`bankBic` VARCHAR(191) NULL,
`bankName` VARCHAR(191) NULL,
`invoicePrefix` VARCHAR(191) NOT NULL DEFAULT 'RE',
`invoiceSequence` INTEGER NOT NULL DEFAULT 0,
`userId` VARCHAR(191) NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `customers` (
`id` VARCHAR(191) NOT NULL,
`companyId` VARCHAR(191) NOT NULL,
`name` VARCHAR(191) NOT NULL,
`vatId` VARCHAR(191) NULL,
`taxId` VARCHAR(191) NULL,
`address` VARCHAR(191) NOT NULL,
`zip` VARCHAR(191) NOT NULL,
`city` VARCHAR(191) NOT NULL,
`country` VARCHAR(191) NOT NULL DEFAULT 'DE',
`email` VARCHAR(191) NULL,
`phone` VARCHAR(191) NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `invoices` (
`id` VARCHAR(191) NOT NULL,
`number` VARCHAR(191) NOT NULL,
`companyId` VARCHAR(191) NOT NULL,
`customerId` VARCHAR(191) NOT NULL,
`issueDate` DATETIME(3) NOT NULL,
`deliveryDate` DATETIME(3) NULL,
`dueDate` DATETIME(3) NOT NULL,
`status` ENUM('DRAFT', 'SENT', 'PAID', 'CANCELLED') NOT NULL DEFAULT 'DRAFT',
`notes` TEXT NULL,
`netTotal` DECIMAL(10, 2) NOT NULL,
`taxTotal` DECIMAL(10, 2) NOT NULL,
`grossTotal` DECIMAL(10, 2) NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
UNIQUE INDEX `invoices_companyId_number_key`(`companyId`, `number`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `invoice_items` (
`id` VARCHAR(191) NOT NULL,
`invoiceId` VARCHAR(191) NOT NULL,
`position` INTEGER NOT NULL,
`description` TEXT NOT NULL,
`quantity` DECIMAL(10, 3) NOT NULL,
`unit` VARCHAR(191) NULL,
`unitPrice` DECIMAL(10, 2) NOT NULL,
`taxRate` DECIMAL(5, 2) NOT NULL,
`netAmount` DECIMAL(10, 2) NOT NULL,
`taxAmount` DECIMAL(10, 2) NOT NULL,
`grossAmount` DECIMAL(10, 2) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `companies` ADD CONSTRAINT `companies_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `customers` ADD CONSTRAINT `customers_companyId_fkey` FOREIGN KEY (`companyId`) REFERENCES `companies`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `invoices` ADD CONSTRAINT `invoices_companyId_fkey` FOREIGN KEY (`companyId`) REFERENCES `companies`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `invoices` ADD CONSTRAINT `invoices_customerId_fkey` FOREIGN KEY (`customerId`) REFERENCES `customers`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `invoice_items` ADD CONSTRAINT `invoice_items_invoiceId_fkey` FOREIGN KEY (`invoiceId`) REFERENCES `invoices`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
+3
View File
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "mysql"
+1 -1
View File
@@ -7,7 +7,7 @@ async function main() {
console.log("Seeding database...");
// Create demo user
const passwordHash = await bcrypt.hash("demo123", 12);
const passwordHash = await bcrypt.hash("annas_password", 12);
const user = await prisma.user.upsert({
where: { email: "anna@example.de" },
update: {},
+5
View File
@@ -0,0 +1,5 @@
import type { Config } from "@react-router/dev/config";
export default {
ssr: true,
} satisfies Config;
-3
View File
@@ -1,3 +0,0 @@
export default function AuthLayout({ children }: { children: React.ReactNode }) {
return children;
}
@@ -1,80 +0,0 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { InvoiceStatus } from "@prisma/client";
import { Download, CheckCircle, Send, Trash2 } from "lucide-react";
interface Props {
invoice: {
id: string;
status: InvoiceStatus;
companyId: string;
};
}
export function InvoiceActions({ invoice }: Props) {
const router = useRouter();
const [loading, setLoading] = useState(false);
async function updateStatus(status: InvoiceStatus) {
setLoading(true);
await fetch(`/api/invoices/${invoice.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status }),
});
setLoading(false);
router.refresh();
}
async function handleDelete() {
if (!confirm("Rechnung wirklich löschen?")) return;
await fetch(`/api/invoices/${invoice.id}`, { method: "DELETE" });
router.push(`/companies/${invoice.companyId}/invoices`);
}
async function downloadPdf() {
const res = await fetch(`/api/invoices/${invoice.id}/pdf`);
if (!res.ok) return;
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `rechnung-${invoice.id}.pdf`;
a.click();
URL.revokeObjectURL(url);
}
return (
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={downloadPdf}>
<Download className="h-4 w-4" /> PDF
</Button>
{invoice.status === "DRAFT" && (
<Button size="sm" onClick={() => updateStatus(InvoiceStatus.SENT)} disabled={loading}>
<Send className="h-4 w-4" /> Als versendet markieren
</Button>
)}
{invoice.status === "SENT" && (
<Button
size="sm"
className="bg-green-600 hover:bg-green-700"
onClick={() => updateStatus(InvoiceStatus.PAID)}
disabled={loading}
>
<CheckCircle className="h-4 w-4" /> Als bezahlt markieren
</Button>
)}
{(invoice.status === "DRAFT" || invoice.status === "CANCELLED") && (
<Button variant="outline" size="sm" className="text-red-600 hover:text-red-700 hover:bg-red-50" onClick={handleDelete}>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
);
}
@@ -1,30 +0,0 @@
"use client";
import { useRouter } from "next/navigation";
import { InvoiceForm } from "@/components/invoice/invoice-form";
interface Props {
customers: { id: string; name: string }[];
companyId: string;
}
export function InvoiceFormClient({ customers, companyId }: Props) {
const router = useRouter();
async function handleSubmit(data: Record<string, unknown>) {
const res = await fetch("/api/invoices", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (res.ok) {
const invoice = await res.json();
router.push(`/companies/${companyId}/invoices/${invoice.id}`);
} else {
alert("Fehler beim Erstellen der Rechnung.");
}
}
return <InvoiceForm customers={customers} companyId={companyId} onSubmit={handleSubmit} />;
}
@@ -1,52 +0,0 @@
import { auth } from "@/lib/auth";
import prisma from "@/lib/prisma";
import { notFound, redirect } from "next/navigation";
import Link from "next/link";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { InvoiceFormClient } from "./invoice-form-client";
import { ChevronLeft } from "lucide-react";
export default async function NewInvoicePage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const session = await auth();
const company = await prisma.company.findFirst({
where: { id, userId: session!.user!.id! },
});
if (!company) notFound();
const customers = await prisma.customer.findMany({
where: { companyId: id },
orderBy: { name: "asc" },
select: { id: true, name: true },
});
if (customers.length === 0) {
redirect(`/companies/${id}/customers`);
}
return (
<div>
<Link
href={`/companies/${id}/invoices`}
className="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700 mb-6"
>
<ChevronLeft className="h-4 w-4" /> Zurück zu Rechnungen
</Link>
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Neue Rechnung</h1>
<p className="text-gray-500 mt-1">Für Mandant: {company.name}</p>
</div>
<Card>
<CardHeader>
<CardTitle>Rechnungsdaten</CardTitle>
</CardHeader>
<CardContent>
<InvoiceFormClient customers={customers} companyId={id} />
</CardContent>
</Card>
</div>
);
}
-23
View File
@@ -1,23 +0,0 @@
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
import { Sidebar } from "@/components/layout/sidebar";
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await auth();
if (!session?.user) redirect("/login");
return (
<div className="flex min-h-screen bg-gray-50">
<Sidebar userName={session.user.name} />
<main className="flex-1 overflow-auto">
<div className="max-w-7xl mx-auto px-6 py-8">
{children}
</div>
</main>
</div>
);
}
-3
View File
@@ -1,3 +0,0 @@
import { handlers } from "@/lib/auth";
export const { GET, POST } = handlers;
@@ -1,23 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import prisma from "@/lib/prisma";
export async function GET(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await auth();
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { id } = await params;
// Verify company belongs to user
const company = await prisma.company.findFirst({
where: { id, userId: session.user.id },
});
if (!company) return NextResponse.json({ error: "Not found" }, { status: 404 });
const customers = await prisma.customer.findMany({
where: { companyId: id },
orderBy: { name: "asc" },
});
return NextResponse.json(customers);
}
@@ -1,23 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import prisma from "@/lib/prisma";
export async function GET(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await auth();
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { id } = await params;
const company = await prisma.company.findFirst({
where: { id, userId: session.user.id },
});
if (!company) return NextResponse.json({ error: "Not found" }, { status: 404 });
const invoices = await prisma.invoice.findMany({
where: { companyId: id },
include: { customer: { select: { name: true } } },
orderBy: { issueDate: "desc" },
});
return NextResponse.json(invoices);
}
-70
View File
@@ -1,70 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import prisma from "@/lib/prisma";
import { z } from "zod";
const companySchema = z.object({
name: z.string().min(1),
legalForm: z.string().optional(),
taxId: z.string().optional(),
vatId: z.string().optional(),
address: z.string().min(1),
zip: z.string().min(1),
city: z.string().min(1),
country: z.string().optional(),
email: z.string().email().optional().or(z.literal("")),
phone: z.string().optional(),
website: z.string().optional(),
bankIban: z.string().optional(),
bankBic: z.string().optional(),
bankName: z.string().optional(),
invoicePrefix: z.string().optional(),
});
async function getCompany(id: string, userId: string) {
return prisma.company.findFirst({ where: { id, userId } });
}
export async function GET(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await auth();
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { id } = await params;
const company = await getCompany(id, session.user.id);
if (!company) return NextResponse.json({ error: "Not found" }, { status: 404 });
return NextResponse.json(company);
}
export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await auth();
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { id } = await params;
const company = await getCompany(id, session.user.id);
if (!company) return NextResponse.json({ error: "Not found" }, { status: 404 });
const body = await req.json();
const parsed = companySchema.safeParse(body);
if (!parsed.success) return NextResponse.json({ error: parsed.error.issues }, { status: 400 });
const updated = await prisma.company.update({
where: { id },
data: parsed.data,
});
return NextResponse.json(updated);
}
export async function DELETE(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await auth();
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { id } = await params;
const company = await getCompany(id, session.user.id);
if (!company) return NextResponse.json({ error: "Not found" }, { status: 404 });
await prisma.company.delete({ where: { id } });
return NextResponse.json({ ok: true });
}
-56
View File
@@ -1,56 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import prisma from "@/lib/prisma";
import { z } from "zod";
const customerSchema = z.object({
name: z.string().min(1),
vatId: z.string().optional(),
taxId: z.string().optional(),
address: z.string().min(1),
zip: z.string().min(1),
city: z.string().min(1),
country: z.string().optional(),
email: z.string().email().optional().or(z.literal("")),
phone: z.string().optional(),
});
async function getCustomer(id: string, userId: string) {
return prisma.customer.findFirst({
where: { id, company: { userId } },
});
}
export async function GET(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await auth();
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { id } = await params;
const customer = await getCustomer(id, session.user.id);
if (!customer) return NextResponse.json({ error: "Not found" }, { status: 404 });
return NextResponse.json(customer);
}
export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await auth();
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { id } = await params;
const customer = await getCustomer(id, session.user.id);
if (!customer) return NextResponse.json({ error: "Not found" }, { status: 404 });
const body = await req.json();
const parsed = customerSchema.safeParse(body);
if (!parsed.success) return NextResponse.json({ error: parsed.error.issues }, { status: 400 });
const updated = await prisma.customer.update({ where: { id }, data: parsed.data });
return NextResponse.json(updated);
}
export async function DELETE(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await auth();
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { id } = await params;
const customer = await getCustomer(id, session.user.id);
if (!customer) return NextResponse.json({ error: "Not found" }, { status: 404 });
await prisma.customer.delete({ where: { id } });
return NextResponse.json({ ok: true });
}
-35
View File
@@ -1,35 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import prisma from "@/lib/prisma";
import { z } from "zod";
const customerSchema = z.object({
companyId: z.string().min(1),
name: z.string().min(1),
vatId: z.string().optional(),
taxId: z.string().optional(),
address: z.string().min(1),
zip: z.string().min(1),
city: z.string().min(1),
country: z.string().optional().default("DE"),
email: z.string().email().optional().or(z.literal("")),
phone: z.string().optional(),
});
export async function POST(req: NextRequest) {
const session = await auth();
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const body = await req.json();
const parsed = customerSchema.safeParse(body);
if (!parsed.success) return NextResponse.json({ error: parsed.error.issues }, { status: 400 });
// Verify company belongs to user
const company = await prisma.company.findFirst({
where: { id: parsed.data.companyId, userId: session.user.id },
});
if (!company) return NextResponse.json({ error: "Company not found" }, { status: 404 });
const customer = await prisma.customer.create({ data: parsed.data });
return NextResponse.json(customer, { status: 201 });
}
-57
View File
@@ -1,57 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import prisma from "@/lib/prisma";
import { InvoiceStatus } from "@prisma/client";
import { z } from "zod";
async function getInvoice(id: string, userId: string) {
return prisma.invoice.findFirst({
where: { id, company: { userId } },
include: {
items: { orderBy: { position: "asc" } },
customer: true,
company: true,
},
});
}
export async function GET(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await auth();
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { id } = await params;
const invoice = await getInvoice(id, session.user.id);
if (!invoice) return NextResponse.json({ error: "Not found" }, { status: 404 });
return NextResponse.json(invoice);
}
const statusSchema = z.object({
status: z.nativeEnum(InvoiceStatus),
});
export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await auth();
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { id } = await params;
const invoice = await getInvoice(id, session.user.id);
if (!invoice) return NextResponse.json({ error: "Not found" }, { status: 404 });
const body = await req.json();
const parsed = statusSchema.safeParse(body);
if (!parsed.success) return NextResponse.json({ error: parsed.error.issues }, { status: 400 });
const updated = await prisma.invoice.update({
where: { id },
data: { status: parsed.data.status },
});
return NextResponse.json(updated);
}
export async function DELETE(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await auth();
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { id } = await params;
const invoice = await getInvoice(id, session.user.id);
if (!invoice) return NextResponse.json({ error: "Not found" }, { status: 404 });
await prisma.invoice.delete({ where: { id } });
return NextResponse.json({ ok: true });
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

-19
View File
@@ -1,19 +0,0 @@
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "Annas Rechnungsmanager",
description: "Buchhaltung & Rechnungsverwaltung für Mandanten",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="de">
<body>{children}</body>
</html>
);
}
-56
View File
@@ -1,56 +0,0 @@
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import bcrypt from "bcryptjs";
import prisma from "./prisma";
export const { handlers, auth, signIn, signOut } = NextAuth({
session: { strategy: "jwt" },
pages: {
signIn: "/login",
},
providers: [
Credentials({
name: "credentials",
credentials: {
email: { label: "E-Mail", type: "email" },
password: { label: "Passwort", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) return null;
const user = await prisma.user.findUnique({
where: { email: credentials.email as string },
});
if (!user) return null;
const passwordValid = await bcrypt.compare(
credentials.password as string,
user.passwordHash
);
if (!passwordValid) return null;
return {
id: user.id,
email: user.email,
name: user.name,
};
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id;
}
return token;
},
async session({ session, token }) {
if (token && session.user) {
session.user.id = token.id as string;
}
return session;
},
},
});
-24
View File
@@ -1,24 +0,0 @@
import { auth } from "@/lib/auth";
import { NextResponse } from "next/server";
export default auth((req) => {
const isLoggedIn = !!req.auth;
const isAuthPage = req.nextUrl.pathname.startsWith("/login");
const isApiAuth = req.nextUrl.pathname.startsWith("/api/auth");
if (isApiAuth) return NextResponse.next();
if (!isLoggedIn && !isAuthPage) {
return NextResponse.redirect(new URL("/login", req.nextUrl));
}
if (isLoggedIn && isAuthPage) {
return NextResponse.redirect(new URL("/", req.nextUrl));
}
return NextResponse.next();
});
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico|public).*)"],
};

Some files were not shown because too many files have changed in this diff Show More