ADD: changed to rect router
This commit is contained in:
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// Generated by React Router
|
||||||
|
|
||||||
|
import "react-router";
|
||||||
|
|
||||||
|
declare module "react-router" {
|
||||||
|
interface Future {
|
||||||
|
v8_middleware: false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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"];
|
||||||
|
}
|
||||||
@@ -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"];
|
||||||
|
}
|
||||||
@@ -11,18 +11,19 @@ Buchhaltungs- und Rechnungsverwaltungssystem für Mandanten.
|
|||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- **Next.js 14** (App Router) + TypeScript
|
- **React Router v7** (Framework Mode, SSR) + TypeScript
|
||||||
- **MySQL / MariaDB** via Prisma ORM
|
- **MariaDB / MySQL** via Prisma ORM
|
||||||
- **NextAuth.js v5** (Email/Passwort-Login)
|
- **Cookie-Session-Auth** (bcryptjs, kein NextAuth)
|
||||||
- **Tailwind CSS** + shadcn/ui
|
- **Tailwind CSS v4** + shadcn/ui
|
||||||
- **@react-pdf/renderer** für PDF-Generierung
|
- **@react-pdf/renderer** für PDF-Generierung
|
||||||
|
- **Docker** für die Datenbank
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
### 1. Voraussetzungen
|
### 1. Voraussetzungen
|
||||||
|
|
||||||
- Node.js 18+
|
- Node.js 20+
|
||||||
- MySQL oder MariaDB
|
- Docker (für MariaDB)
|
||||||
|
|
||||||
### 2. Installation
|
### 2. Installation
|
||||||
|
|
||||||
@@ -30,11 +31,11 @@ Buchhaltungs- und Rechnungsverwaltungssystem für Mandanten.
|
|||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Datenbank konfigurieren
|
### 3. Umgebungsvariablen konfigurieren
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# DATABASE_URL in .env anpassen
|
# DATABASE_URL und AUTH_SECRET in .env anpassen
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Datenbank einrichten
|
### 4. Datenbank einrichten
|
||||||
@@ -52,7 +53,9 @@ npx prisma db seed
|
|||||||
npm run dev
|
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
|
## Datenbank-Kommandos
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback } from "react";
|
||||||
import { useForm, useFieldArray } from "react-hook-form";
|
import { useForm, useFieldArray } from "react-hook-form";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -128,7 +126,6 @@ export function InvoiceForm({ customers, companyId, onSubmit }: InvoiceFormProps
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-6">
|
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-6">
|
||||||
{/* Header info */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label>Kunde *</Label>
|
<Label>Kunde *</Label>
|
||||||
@@ -161,7 +158,6 @@ export function InvoiceForm({ customers, companyId, onSubmit }: InvoiceFormProps
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Items */}
|
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide">Rechnungspositionen</h3>
|
<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>
|
||||||
|
|
||||||
<div className="border border-gray-200 rounded-xl overflow-hidden">
|
<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="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-4">Beschreibung</div>
|
||||||
<div className="col-span-1">Menge</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 })}
|
{...register(`items.${index}.description`, { required: true })}
|
||||||
placeholder="Leistungsbeschreibung"
|
placeholder="Leistungsbeschreibung"
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
onChange={(e) => {
|
|
||||||
register(`items.${index}.description`).onChange(e);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-1">
|
<div className="col-span-1">
|
||||||
@@ -260,7 +252,6 @@ export function InvoiceForm({ customers, companyId, onSubmit }: InvoiceFormProps
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Totals */}
|
|
||||||
<div className="mt-4 flex justify-end">
|
<div className="mt-4 flex justify-end">
|
||||||
<div className="w-64 space-y-1.5">
|
<div className="w-64 space-y-1.5">
|
||||||
<div className="flex justify-between text-sm text-gray-600">
|
<div className="flex justify-between text-sm text-gray-600">
|
||||||
@@ -279,7 +270,6 @@ export function InvoiceForm({ customers, companyId, onSubmit }: InvoiceFormProps
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Notes */}
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label>Anmerkungen</Label>
|
<Label>Anmerkungen</Label>
|
||||||
<Textarea {...register("notes")} placeholder="Zahlungsbedingungen, Hinweise..." rows={3} />
|
<Textarea {...register("notes")} placeholder="Zahlungsbedingungen, Hinweise..." rows={3} />
|
||||||
@@ -246,7 +246,6 @@ interface InvoicePDFProps {
|
|||||||
export function InvoicePDFDocument({ invoice }: InvoicePDFProps) {
|
export function InvoicePDFDocument({ invoice }: InvoicePDFProps) {
|
||||||
const n = (v: unknown) => Number(v);
|
const n = (v: unknown) => Number(v);
|
||||||
|
|
||||||
// Group items by tax rate
|
|
||||||
const taxGroups = invoice.items.reduce(
|
const taxGroups = invoice.items.reduce(
|
||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
const rate = n(item.taxRate);
|
const rate = n(item.taxRate);
|
||||||
@@ -261,7 +260,6 @@ export function InvoicePDFDocument({ invoice }: InvoicePDFProps) {
|
|||||||
return (
|
return (
|
||||||
<Document>
|
<Document>
|
||||||
<Page size="A4" style={styles.page}>
|
<Page size="A4" style={styles.page}>
|
||||||
{/* Header */}
|
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<View>
|
<View>
|
||||||
<Text style={styles.companyName}>{invoice.company.name}</Text>
|
<Text style={styles.companyName}>{invoice.company.name}</Text>
|
||||||
@@ -287,10 +285,8 @@ export function InvoicePDFDocument({ invoice }: InvoicePDFProps) {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Invoice Title */}
|
|
||||||
<Text style={styles.invoiceTitle}>Rechnung</Text>
|
<Text style={styles.invoiceTitle}>Rechnung</Text>
|
||||||
|
|
||||||
{/* Address Section */}
|
|
||||||
<View style={styles.addressSection}>
|
<View style={styles.addressSection}>
|
||||||
<View style={styles.addressBlock}>
|
<View style={styles.addressBlock}>
|
||||||
<Text style={styles.addressLabel}>Rechnungsempfänger</Text>
|
<Text style={styles.addressLabel}>Rechnungsempfänger</Text>
|
||||||
@@ -308,7 +304,6 @@ export function InvoicePDFDocument({ invoice }: InvoicePDFProps) {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Meta */}
|
|
||||||
<View style={styles.metaGrid}>
|
<View style={styles.metaGrid}>
|
||||||
<View style={styles.metaItem}>
|
<View style={styles.metaItem}>
|
||||||
<Text style={styles.metaLabel}>Rechnungsnummer</Text>
|
<Text style={styles.metaLabel}>Rechnungsnummer</Text>
|
||||||
@@ -330,7 +325,6 @@ export function InvoicePDFDocument({ invoice }: InvoicePDFProps) {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Items Table */}
|
|
||||||
<View style={styles.tableHeader}>
|
<View style={styles.tableHeader}>
|
||||||
<Text style={{ ...styles.tableHeaderText, ...styles.col_pos }}>#</Text>
|
<Text style={{ ...styles.tableHeaderText, ...styles.col_pos }}>#</Text>
|
||||||
<Text style={{ ...styles.tableHeaderText, ...styles.col_desc }}>Beschreibung</Text>
|
<Text style={{ ...styles.tableHeaderText, ...styles.col_desc }}>Beschreibung</Text>
|
||||||
@@ -355,7 +349,6 @@ export function InvoicePDFDocument({ invoice }: InvoicePDFProps) {
|
|||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Totals */}
|
|
||||||
<View style={styles.totalsSection}>
|
<View style={styles.totalsSection}>
|
||||||
<View style={styles.totalsTable}>
|
<View style={styles.totalsTable}>
|
||||||
<View style={styles.totalsRow}>
|
<View style={styles.totalsRow}>
|
||||||
@@ -375,7 +368,6 @@ export function InvoicePDFDocument({ invoice }: InvoicePDFProps) {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Notes */}
|
|
||||||
{invoice.notes && (
|
{invoice.notes && (
|
||||||
<View style={styles.notes}>
|
<View style={styles.notes}>
|
||||||
<Text style={styles.notesLabel}>Hinweise</Text>
|
<Text style={styles.notesLabel}>Hinweise</Text>
|
||||||
@@ -383,7 +375,6 @@ export function InvoicePDFDocument({ invoice }: InvoicePDFProps) {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<View style={styles.footer} fixed>
|
<View style={styles.footer} fixed>
|
||||||
<Text style={styles.footerText}>
|
<Text style={styles.footerText}>
|
||||||
{invoice.company.name}
|
{invoice.company.name}
|
||||||
@@ -1,12 +1,7 @@
|
|||||||
"use client";
|
import { Link, Form, useLocation } from "react-router";
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
import { signOut } from "next-auth/react";
|
|
||||||
import {
|
import {
|
||||||
Calculator,
|
Calculator,
|
||||||
Building2,
|
Building2,
|
||||||
FileText,
|
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
LogOut,
|
LogOut,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
@@ -26,11 +21,11 @@ const navItems: NavItem[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export function Sidebar({ userName }: { userName?: string | null }) {
|
export function Sidebar({ userName }: { userName?: string | null }) {
|
||||||
const pathname = usePathname();
|
const location = useLocation();
|
||||||
|
const pathname = location.pathname;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="w-60 shrink-0 flex flex-col bg-white border-r border-gray-200 min-h-screen">
|
<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 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">
|
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-indigo-600">
|
||||||
<Calculator className="w-4 h-4 text-white" />
|
<Calculator className="w-4 h-4 text-white" />
|
||||||
@@ -40,14 +35,13 @@ export function Sidebar({ userName }: { userName?: string | null }) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Nav */}
|
|
||||||
<nav className="flex-1 px-3 py-4 space-y-0.5">
|
<nav className="flex-1 px-3 py-4 space-y-0.5">
|
||||||
{navItems.map((item) => {
|
{navItems.map((item) => {
|
||||||
const active = pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href));
|
const active = pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href));
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
to={item.href}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors",
|
"flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors",
|
||||||
active
|
active
|
||||||
@@ -63,20 +57,21 @@ export function Sidebar({ userName }: { userName?: string | null }) {
|
|||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* User */}
|
|
||||||
<div className="px-3 py-4 border-t border-gray-200">
|
<div className="px-3 py-4 border-t border-gray-200">
|
||||||
{userName && (
|
{userName && (
|
||||||
<p className="text-xs text-gray-500 px-3 mb-2 truncate">{userName}</p>
|
<p className="text-xs text-gray-500 px-3 mb-2 truncate">{userName}</p>
|
||||||
)}
|
)}
|
||||||
|
<Form method="post" action="/logout">
|
||||||
<Button
|
<Button
|
||||||
|
type="submit"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-full justify-start text-gray-600 hover:text-red-600 hover:bg-red-50"
|
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" />
|
<LogOut className="h-4 w-4" />
|
||||||
Abmelden
|
Abmelden
|
||||||
</Button>
|
</Button>
|
||||||
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||||
import { Check, ChevronDown } from "lucide-react";
|
import { Check, ChevronDown } from "lucide-react";
|
||||||
@@ -3,7 +3,6 @@ import prisma from "./prisma";
|
|||||||
export async function generateInvoiceNumber(companyId: string): Promise<string> {
|
export async function generateInvoiceNumber(companyId: string): Promise<string> {
|
||||||
const year = new Date().getFullYear();
|
const year = new Date().getFullYear();
|
||||||
|
|
||||||
// Atomically increment the sequence number
|
|
||||||
const company = await prisma.company.update({
|
const company = await prisma.company.update({
|
||||||
where: { id: companyId },
|
where: { id: companyId },
|
||||||
data: { invoiceSequence: { increment: 1 } },
|
data: { invoiceSequence: { increment: 1 } },
|
||||||
@@ -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 />;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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 { getApiUser } from "@/session.server";
|
||||||
import { auth } from "@/lib/auth";
|
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -21,32 +20,32 @@ const companySchema = z.object({
|
|||||||
invoicePrefix: z.string().optional().default("RE"),
|
invoicePrefix: z.string().optional().default("RE"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function GET() {
|
export async function loader({ request }: { request: Request }) {
|
||||||
const session = await auth();
|
const user = await getApiUser(request);
|
||||||
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
||||||
const companies = await prisma.company.findMany({
|
const companies = await prisma.company.findMany({
|
||||||
where: { userId: session.user.id },
|
where: { userId: user.id },
|
||||||
include: { _count: { select: { invoices: true, customers: true } } },
|
include: { _count: { select: { invoices: true, customers: true } } },
|
||||||
orderBy: { name: "asc" },
|
orderBy: { name: "asc" },
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json(companies);
|
return Response.json(companies);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function action({ request }: { request: Request }) {
|
||||||
const session = await auth();
|
const user = await getApiUser(request);
|
||||||
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
||||||
const body = await req.json();
|
const body = await request.json();
|
||||||
const parsed = companySchema.safeParse(body);
|
const parsed = companySchema.safeParse(body);
|
||||||
if (!parsed.success) {
|
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({
|
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 });
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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 { getApiUser } from "@/session.server";
|
||||||
import { auth } from "@/lib/auth";
|
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
export async function GET(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
export async function loader({ request, params }: { request: Request; params: { id: string } }) {
|
||||||
const session = await auth();
|
const user = await getApiUser(request);
|
||||||
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
||||||
const { id } = await params;
|
|
||||||
|
|
||||||
const invoice = await prisma.invoice.findFirst({
|
const invoice = await prisma.invoice.findFirst({
|
||||||
where: { id, company: { userId: session.user.id } },
|
where: { id: params.id, company: { userId: user.id } },
|
||||||
include: {
|
include: {
|
||||||
items: { orderBy: { position: "asc" } },
|
items: { orderBy: { position: "asc" } },
|
||||||
customer: true,
|
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 { renderToBuffer } = await import("@react-pdf/renderer");
|
||||||
const React = (await import("react")).default;
|
const React = (await import("react")).default;
|
||||||
const { InvoicePDFDocument } = await import("@/components/invoice/invoice-pdf");
|
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 element = React.createElement(InvoicePDFDocument as any, { invoice }) as any;
|
||||||
const buffer = await renderToBuffer(element);
|
const buffer = await renderToBuffer(element);
|
||||||
|
|
||||||
return new NextResponse(new Uint8Array(buffer), {
|
return new Response(new Uint8Array(buffer), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/pdf",
|
"Content-Type": "application/pdf",
|
||||||
@@ -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 { getApiUser } from "@/session.server";
|
||||||
import { auth } from "@/lib/auth";
|
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { generateInvoiceNumber } from "@/lib/invoice-number";
|
import { generateInvoiceNumber } from "@/lib/invoice-number";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -29,21 +28,18 @@ const invoiceSchema = z.object({
|
|||||||
grossTotal: z.number(),
|
grossTotal: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function action({ request }: { request: Request }) {
|
||||||
const session = await auth();
|
const user = await getApiUser(request);
|
||||||
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
||||||
const body = await req.json();
|
const body = await request.json();
|
||||||
const parsed = invoiceSchema.safeParse(body);
|
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;
|
const { items, companyId, ...invoiceData } = parsed.data;
|
||||||
|
|
||||||
// Verify company belongs to user
|
const company = await prisma.company.findFirst({ where: { id: companyId, userId: user.id } });
|
||||||
const company = await prisma.company.findFirst({
|
if (!company) return Response.json({ error: "Company not found" }, { status: 404 });
|
||||||
where: { id: companyId, userId: session.user.id },
|
|
||||||
});
|
|
||||||
if (!company) return NextResponse.json({ error: "Company not found" }, { status: 404 });
|
|
||||||
|
|
||||||
const number = await generateInvoiceNumber(companyId);
|
const number = await generateInvoiceNumber(companyId);
|
||||||
|
|
||||||
@@ -55,20 +51,10 @@ export async function POST(req: NextRequest) {
|
|||||||
issueDate: new Date(invoiceData.issueDate),
|
issueDate: new Date(invoiceData.issueDate),
|
||||||
deliveryDate: invoiceData.deliveryDate ? new Date(invoiceData.deliveryDate) : null,
|
deliveryDate: invoiceData.deliveryDate ? new Date(invoiceData.deliveryDate) : null,
|
||||||
dueDate: new Date(invoiceData.dueDate),
|
dueDate: new Date(invoiceData.dueDate),
|
||||||
items: {
|
items: { create: items },
|
||||||
create: items.map((item) => ({
|
|
||||||
...item,
|
|
||||||
quantity: item.quantity,
|
|
||||||
unitPrice: item.unitPrice,
|
|
||||||
taxRate: item.taxRate,
|
|
||||||
netAmount: item.netAmount,
|
|
||||||
taxAmount: item.taxAmount,
|
|
||||||
grossAmount: item.grossAmount,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
include: { items: true, customer: true },
|
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 { getApiUser } from "@/session.server";
|
||||||
import { auth } from "@/lib/auth";
|
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { InvoiceStatus } from "@prisma/client";
|
import { InvoiceStatus } from "@prisma/client";
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
export async function loader({ request }: { request: Request }) {
|
||||||
const session = await auth();
|
const user = await getApiUser(request);
|
||||||
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
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 companyId = searchParams.get("companyId");
|
||||||
const year = parseInt(searchParams.get("year") ?? String(new Date().getFullYear()));
|
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({
|
const company = await prisma.company.findFirst({ where: { id: companyId, userId: user.id } });
|
||||||
where: { id: companyId, userId: session.user.id },
|
if (!company) return Response.json({ error: "Not found" }, { status: 404 });
|
||||||
});
|
|
||||||
if (!company) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
||||||
|
|
||||||
// Get all paid/sent invoices for the year
|
|
||||||
const invoices = await prisma.invoice.findMany({
|
const invoices = await prisma.invoice.findMany({
|
||||||
where: {
|
where: {
|
||||||
companyId,
|
companyId,
|
||||||
@@ -28,14 +24,10 @@ export async function GET(req: NextRequest) {
|
|||||||
lt: new Date(`${year + 1}-01-01`),
|
lt: new Date(`${year + 1}-01-01`),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: {
|
include: { items: true, customer: { select: { name: true } } },
|
||||||
items: true,
|
|
||||||
customer: { select: { name: true } },
|
|
||||||
},
|
|
||||||
orderBy: { issueDate: "asc" },
|
orderBy: { issueDate: "asc" },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build monthly summary
|
|
||||||
const monthly: Record<number, {
|
const monthly: Record<number, {
|
||||||
month: number;
|
month: number;
|
||||||
invoiceCount: number;
|
invoiceCount: number;
|
||||||
@@ -65,7 +57,6 @@ export async function GET(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quarterly
|
|
||||||
const quarterly = [1, 2, 3, 4].map((q) => {
|
const quarterly = [1, 2, 3, 4].map((q) => {
|
||||||
const months = [q * 3 - 2, q * 3 - 1, q * 3];
|
const months = [q * 3 - 2, q * 3 - 1, q * 3];
|
||||||
const data = months.map((m) => monthly[m]);
|
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),
|
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 });
|
||||||
}
|
}
|
||||||
+37
-43
@@ -1,8 +1,7 @@
|
|||||||
"use client";
|
import { useState } from "react";
|
||||||
|
import { Link, useLoaderData, useParams, useRevalidator } from "react-router";
|
||||||
import { useEffect, useState } from "react";
|
import { requireUser } from "@/session.server";
|
||||||
import { useParams } from "next/navigation";
|
import prisma from "@/lib/prisma";
|
||||||
import Link from "next/link";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -28,23 +27,27 @@ type FormData = z.infer<typeof schema>;
|
|||||||
interface Customer {
|
interface Customer {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
vatId?: string | null | undefined;
|
vatId?: string | null;
|
||||||
address: string;
|
address: string;
|
||||||
zip: string;
|
zip: string;
|
||||||
city: string;
|
city: string;
|
||||||
email?: string | null | undefined;
|
email?: string | null;
|
||||||
phone?: string | null | undefined;
|
phone?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CustomerForEdit {
|
export async function loader({ request, params }: { request: Request; params: { id: string } }) {
|
||||||
id: string;
|
const user = await requireUser(request);
|
||||||
name: string;
|
const company = await prisma.company.findFirst({
|
||||||
vatId?: string | undefined;
|
where: { id: params.id, userId: user.id },
|
||||||
address: string;
|
});
|
||||||
zip: string;
|
if (!company) throw new Response("Not Found", { status: 404 });
|
||||||
city: string;
|
|
||||||
email?: string | undefined;
|
const customers = await prisma.customer.findMany({
|
||||||
phone?: string | undefined;
|
where: { companyId: params.id },
|
||||||
|
orderBy: { name: "asc" },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { customers, companyId: params.id };
|
||||||
}
|
}
|
||||||
|
|
||||||
function CustomerForm({
|
function CustomerForm({
|
||||||
@@ -103,17 +106,10 @@ function CustomerForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function CustomersPage() {
|
export default function CustomersPage() {
|
||||||
const { id: companyId } = useParams<{ id: string }>();
|
const { customers, companyId } = useLoaderData<typeof loader>();
|
||||||
const [customers, setCustomers] = useState<Customer[]>([]);
|
const { revalidate } = useRevalidator();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [editCustomer, setEditCustomer] = useState<CustomerForEdit | null>(null);
|
const [editCustomer, setEditCustomer] = useState<Customer | null>(null);
|
||||||
|
|
||||||
async function load() {
|
|
||||||
const res = await fetch(`/api/companies/${companyId}/customers`);
|
|
||||||
if (res.ok) setCustomers(await res.json());
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => { load(); }, [companyId]);
|
|
||||||
|
|
||||||
async function handleCreate(data: FormData) {
|
async function handleCreate(data: FormData) {
|
||||||
await fetch("/api/customers", {
|
await fetch("/api/customers", {
|
||||||
@@ -122,7 +118,7 @@ export default function CustomersPage() {
|
|||||||
body: JSON.stringify({ ...data, companyId }),
|
body: JSON.stringify({ ...data, companyId }),
|
||||||
});
|
});
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
load();
|
revalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleEdit(data: FormData) {
|
async function handleEdit(data: FormData) {
|
||||||
@@ -133,19 +129,19 @@ export default function CustomersPage() {
|
|||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
setEditCustomer(null);
|
setEditCustomer(null);
|
||||||
load();
|
revalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDelete(customerId: string) {
|
async function handleDelete(customerId: string) {
|
||||||
if (!confirm("Kunden wirklich löschen?")) return;
|
if (!confirm("Kunden wirklich löschen?")) return;
|
||||||
await fetch(`/api/customers/${customerId}`, { method: "DELETE" });
|
await fetch(`/api/customers/${customerId}`, { method: "DELETE" });
|
||||||
load();
|
revalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Link
|
<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"
|
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
|
<ChevronLeft className="h-4 w-4" /> Zurück zum Mandanten
|
||||||
@@ -169,7 +165,6 @@ export default function CustomersPage() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Edit Dialog */}
|
|
||||||
<Dialog open={!!editCustomer} onOpenChange={(o) => !o && setEditCustomer(null)}>
|
<Dialog open={!!editCustomer} onOpenChange={(o) => !o && setEditCustomer(null)}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -177,7 +172,15 @@ export default function CustomersPage() {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{editCustomer && (
|
{editCustomer && (
|
||||||
<CustomerForm
|
<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}
|
onSubmit={handleEdit}
|
||||||
submitLabel="Speichern"
|
submitLabel="Speichern"
|
||||||
/>
|
/>
|
||||||
@@ -219,16 +222,7 @@ export default function CustomersPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => setEditCustomer({
|
onClick={() => setEditCustomer(customer)}
|
||||||
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,
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
+17
-17
@@ -1,36 +1,36 @@
|
|||||||
"use client";
|
import { Link, useLoaderData, useNavigate } from "react-router";
|
||||||
|
import { requireUser } from "@/session.server";
|
||||||
import { useEffect, useState } from "react";
|
import prisma from "@/lib/prisma";
|
||||||
import { useRouter, useParams } from "next/navigation";
|
|
||||||
import { CompanyForm } from "@/components/company/company-form";
|
import { CompanyForm } from "@/components/company/company-form";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import Link from "next/link";
|
|
||||||
import { ChevronLeft } from "lucide-react";
|
import { ChevronLeft } from "lucide-react";
|
||||||
|
|
||||||
export default function EditCompanyPage() {
|
export async function loader({ request, params }: { request: Request; params: { id: string } }) {
|
||||||
const router = useRouter();
|
const user = await requireUser(request);
|
||||||
const { id } = useParams<{ id: string }>();
|
const company = await prisma.company.findFirst({
|
||||||
const [company, setCompany] = useState<Record<string, unknown> | null>(null);
|
where: { id: params.id, userId: user.id },
|
||||||
|
});
|
||||||
|
if (!company) throw new Response("Not Found", { status: 404 });
|
||||||
|
return { company };
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
export default function EditCompanyPage() {
|
||||||
fetch(`/api/companies/${id}`).then((r) => r.json()).then(setCompany);
|
const { company } = useLoaderData<typeof loader>();
|
||||||
}, [id]);
|
const navigate = useNavigate();
|
||||||
|
|
||||||
async function handleSubmit(data: Record<string, unknown>) {
|
async function handleSubmit(data: Record<string, unknown>) {
|
||||||
const res = await fetch(`/api/companies/${id}`, {
|
const res = await fetch(`/api/companies/${company.id}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(data),
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Link
|
<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"
|
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
|
<ChevronLeft className="h-4 w-4" /> Zurück zum Mandanten
|
||||||
+117
-33
@@ -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 prisma from "@/lib/prisma";
|
||||||
import { notFound } from "next/navigation";
|
import { useState } from "react";
|
||||||
import Link from "next/link";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { InvoiceStatusBadge } from "@/components/invoice/invoice-status-badge";
|
import { InvoiceStatusBadge } from "@/components/invoice/invoice-status-badge";
|
||||||
import { InvoiceActions } from "./invoice-actions";
|
|
||||||
import { formatCurrency, formatDate } from "@/lib/tax";
|
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,
|
||||||
}: {
|
}: {
|
||||||
params: Promise<{ id: string; invoiceId: string }>;
|
request: Request;
|
||||||
|
params: { id: string; invoiceId: string };
|
||||||
}) {
|
}) {
|
||||||
const { id, invoiceId } = await params;
|
const user = await requireUser(request);
|
||||||
const session = await auth();
|
const { id, invoiceId } = params;
|
||||||
|
|
||||||
const invoice = await prisma.invoice.findFirst({
|
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: {
|
include: {
|
||||||
items: { orderBy: { position: "asc" } },
|
items: { orderBy: { position: "asc" } },
|
||||||
customer: true,
|
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(
|
const taxGroups = invoice.items.reduce(
|
||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
const rate = Number(item.taxRate);
|
const rate = item.taxRate;
|
||||||
if (!acc[rate]) acc[rate] = { net: 0, tax: 0 };
|
if (!acc[rate]) acc[rate] = { net: 0, tax: 0 };
|
||||||
acc[rate].net += Number(item.netAmount);
|
acc[rate].net += item.netAmount;
|
||||||
acc[rate].tax += Number(item.taxAmount);
|
acc[rate].tax += item.taxAmount;
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{} as Record<number, { net: number; tax: number }>
|
{} 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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Link
|
<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"
|
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
|
<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)}
|
{invoice.customer.name} · {formatDate(invoice.issueDate)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
<div className="lg:col-span-2 space-y-6">
|
<div className="lg:col-span-2 space-y-6">
|
||||||
{/* Invoice document preview */}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
{/* Sender & Recipient */}
|
|
||||||
<div className="grid grid-cols-2 gap-8 mb-8">
|
<div className="grid grid-cols-2 gap-8 mb-8">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">Absender</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dates */}
|
|
||||||
<div className="grid grid-cols-3 gap-4 mb-8 p-4 bg-gray-50 rounded-lg">
|
<div className="grid grid-cols-3 gap-4 mb-8 p-4 bg-gray-50 rounded-lg">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500">Rechnungsnummer</p>
|
<p className="text-xs text-gray-500">Rechnungsnummer</p>
|
||||||
@@ -114,7 +202,6 @@ export default async function InvoiceDetailPage({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Items table */}
|
|
||||||
<div className="border border-gray-200 rounded-lg overflow-hidden mb-6">
|
<div className="border border-gray-200 rounded-lg overflow-hidden mb-6">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead>
|
<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-500">{item.position}</td>
|
||||||
<td className="px-4 py-3 text-gray-900">{item.description}</td>
|
<td className="px-4 py-3 text-gray-900">{item.description}</td>
|
||||||
<td className="px-4 py-3 text-right text-gray-700">
|
<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>
|
||||||
<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">{formatCurrency(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 text-gray-700">{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 font-medium text-gray-900">{formatCurrency(item.grossAmount)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Totals */}
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<div className="w-72 space-y-1.5">
|
<div className="w-72 space-y-1.5">
|
||||||
<div className="flex justify-between text-sm text-gray-600">
|
<div className="flex justify-between text-sm text-gray-600">
|
||||||
<span>Nettobetrag</span>
|
<span>Nettobetrag</span>
|
||||||
<span>{formatCurrency(Number(invoice.netTotal))}</span>
|
<span>{formatCurrency(invoice.netTotal)}</span>
|
||||||
</div>
|
</div>
|
||||||
{Object.entries(taxGroups).map(([rate, { net, tax }]) => (
|
{Object.entries(taxGroups).map(([rate, { net, tax }]) => (
|
||||||
<div key={rate} className="flex justify-between text-sm text-gray-600">
|
<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">
|
<div className="flex justify-between text-base font-bold text-gray-900 border-t border-gray-300 pt-2">
|
||||||
<span>Gesamtbetrag (brutto)</span>
|
<span>Gesamtbetrag (brutto)</span>
|
||||||
<span>{formatCurrency(Number(invoice.grossTotal))}</span>
|
<span>{formatCurrency(invoice.grossTotal)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -171,7 +257,6 @@ export default async function InvoiceDetailPage({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Bank details */}
|
|
||||||
{invoice.company.bankIban && (
|
{invoice.company.bankIban && (
|
||||||
<div className="mt-4 pt-4 border-t border-gray-100">
|
<div className="mt-4 pt-4 border-t border-gray-100">
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">
|
||||||
@@ -185,7 +270,6 @@ export default async function InvoiceDetailPage({
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sidebar */}
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -194,15 +278,15 @@ export default async function InvoiceDetailPage({
|
|||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500">Netto</p>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500">MwSt.</p>
|
<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>
|
||||||
<div className="border-t border-gray-200 pt-2">
|
<div className="border-t border-gray-200 pt-2">
|
||||||
<p className="text-xs text-gray-500">Brutto</p>
|
<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>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+27
-13
@@ -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 prisma from "@/lib/prisma";
|
||||||
import { notFound } from "next/navigation";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { InvoiceStatusBadge } from "@/components/invoice/invoice-status-badge";
|
import { InvoiceStatusBadge } from "@/components/invoice/invoice-status-badge";
|
||||||
import { formatCurrency, formatDate } from "@/lib/tax";
|
import { formatCurrency, formatDate } from "@/lib/tax";
|
||||||
import { Plus, FileText, ChevronLeft } from "lucide-react";
|
import { Plus, FileText, ChevronLeft } from "lucide-react";
|
||||||
|
|
||||||
export default async function InvoicesPage({ params }: { params: Promise<{ id: string }> }) {
|
export async function loader({ request, params }: { request: Request; params: { id: string } }) {
|
||||||
const { id } = await params;
|
const user = await requireUser(request);
|
||||||
const session = await auth();
|
const { id } = params;
|
||||||
|
|
||||||
const company = await prisma.company.findFirst({
|
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({
|
const invoices = await prisma.invoice.findMany({
|
||||||
where: { companyId: id },
|
where: { companyId: id },
|
||||||
@@ -23,10 +22,25 @@ export default async function InvoicesPage({ params }: { params: Promise<{ id: s
|
|||||||
orderBy: { issueDate: "desc" },
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Link
|
<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"
|
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}
|
<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>
|
<p className="text-gray-500 mt-1">{invoices.length} Rechnungen für {company.name}</p>
|
||||||
</div>
|
</div>
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href={`/companies/${id}/invoices/new`}>
|
<Link to={`/companies/${id}/invoices/new`}>
|
||||||
<Plus className="h-4 w-4" /> Neue Rechnung
|
<Plus className="h-4 w-4" /> Neue Rechnung
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</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>
|
<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>
|
<p className="text-gray-500 mb-6 text-sm">Erstellen Sie die erste Rechnung für diesen Mandanten.</p>
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href={`/companies/${id}/invoices/new`}>
|
<Link to={`/companies/${id}/invoices/new`}>
|
||||||
<Plus className="h-4 w-4" /> Erste Rechnung erstellen
|
<Plus className="h-4 w-4" /> Erste Rechnung erstellen
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -63,7 +77,7 @@ export default async function InvoicesPage({ params }: { params: Promise<{ id: s
|
|||||||
{invoices.map((invoice) => (
|
{invoices.map((invoice) => (
|
||||||
<Link
|
<Link
|
||||||
key={invoice.id}
|
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"
|
className="flex items-center justify-between px-6 py-4 hover:bg-gray-50 transition-colors group"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-4">
|
<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>
|
<p className="text-sm text-gray-500">{formatDate(invoice.issueDate)}</p>
|
||||||
<InvoiceStatusBadge status={invoice.status} />
|
<InvoiceStatusBadge status={invoice.status} />
|
||||||
<p className="font-medium text-gray-900 w-28 text-right">
|
<p className="font-medium text-gray-900 w-28 text-right">
|
||||||
{formatCurrency(Number(invoice.grossTotal))}
|
{formatCurrency(invoice.grossTotal)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
+3
-9
@@ -1,8 +1,5 @@
|
|||||||
"use client";
|
import { useState, useEffect } from "react";
|
||||||
|
import { Link, useParams } from "react-router";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { formatCurrency } from "@/lib/tax";
|
import { formatCurrency } from "@/lib/tax";
|
||||||
import { ChevronLeft, TrendingUp, BarChart3 } from "lucide-react";
|
import { ChevronLeft, TrendingUp, BarChart3 } from "lucide-react";
|
||||||
@@ -62,7 +59,7 @@ export default function ReportsPage() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Link
|
<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"
|
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
|
<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>
|
<div className="text-center text-gray-500 py-12">Lade Auswertung...</div>
|
||||||
) : data && (
|
) : data && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Year Summary */}
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
@@ -114,7 +110,6 @@ export default function ReportsPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quarterly UStVA */}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -173,7 +168,6 @@ export default function ReportsPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Monthly breakdown */}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center gap-2">
|
<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 prisma from "@/lib/prisma";
|
||||||
import { notFound } from "next/navigation";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -26,12 +25,12 @@ const statusVariants: Record<InvoiceStatus, "secondary" | "default" | "success"
|
|||||||
CANCELLED: "destructive",
|
CANCELLED: "destructive",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function CompanyPage({ params }: { params: Promise<{ id: string }> }) {
|
export async function loader({ request, params }: { request: Request; params: { id: string } }) {
|
||||||
const { id } = await params;
|
const user = await requireUser(request);
|
||||||
const session = await auth();
|
const { id } = params;
|
||||||
|
|
||||||
const company = await prisma.company.findFirst({
|
const company = await prisma.company.findFirst({
|
||||||
where: { id, userId: session!.user!.id! },
|
where: { id, userId: user.id },
|
||||||
include: {
|
include: {
|
||||||
invoices: {
|
invoices: {
|
||||||
include: { customer: { select: { name: true } } },
|
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({
|
const revenue = await prisma.invoice.aggregate({
|
||||||
where: { companyId: id, status: InvoiceStatus.PAID },
|
where: { companyId: id, status: InvoiceStatus.PAID },
|
||||||
_sum: { grossTotal: true },
|
_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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-start justify-between mb-8">
|
<div className="flex items-start justify-between mb-8">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<div className="p-3 rounded-xl bg-indigo-50">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" asChild>
|
<Button variant="outline" asChild>
|
||||||
<Link href={`/companies/${id}/edit`}>
|
<Link to={`/companies/${id}/edit`}>
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
Bearbeiten
|
Bearbeiten
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Actions */}
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-8">
|
<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">
|
<Card className="hover:border-indigo-200 hover:shadow-sm transition-all cursor-pointer">
|
||||||
<CardContent className="pt-4 pb-4 flex items-center gap-3">
|
<CardContent className="pt-4 pb-4 flex items-center gap-3">
|
||||||
<div className="p-2 rounded-lg bg-indigo-50">
|
<div className="p-2 rounded-lg bg-indigo-50">
|
||||||
@@ -85,7 +100,7 @@ export default async function CompanyPage({ params }: { params: Promise<{ id: st
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</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">
|
<Card className="hover:border-blue-200 hover:shadow-sm transition-all cursor-pointer">
|
||||||
<CardContent className="pt-4 pb-4 flex items-center gap-3">
|
<CardContent className="pt-4 pb-4 flex items-center gap-3">
|
||||||
<div className="p-2 rounded-lg bg-blue-50">
|
<div className="p-2 rounded-lg bg-blue-50">
|
||||||
@@ -95,7 +110,7 @@ export default async function CompanyPage({ params }: { params: Promise<{ id: st
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</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">
|
<Card className="hover:border-green-200 hover:shadow-sm transition-all cursor-pointer">
|
||||||
<CardContent className="pt-4 pb-4 flex items-center gap-3">
|
<CardContent className="pt-4 pb-4 flex items-center gap-3">
|
||||||
<div className="p-2 rounded-lg bg-green-50">
|
<div className="p-2 rounded-lg bg-green-50">
|
||||||
@@ -105,7 +120,7 @@ export default async function CompanyPage({ params }: { params: Promise<{ id: st
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</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">
|
<Card className="hover:border-purple-200 hover:shadow-sm transition-all cursor-pointer">
|
||||||
<CardContent className="pt-4 pb-4 flex items-center gap-3">
|
<CardContent className="pt-4 pb-4 flex items-center gap-3">
|
||||||
<div className="p-2 rounded-lg bg-purple-50">
|
<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>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
{/* Recent Invoices */}
|
|
||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h2 className="font-semibold text-gray-900">Letzte Rechnungen</h2>
|
<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 →
|
Alle anzeigen →
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,7 +151,7 @@ export default async function CompanyPage({ params }: { params: Promise<{ id: st
|
|||||||
{company.invoices.map((invoice) => (
|
{company.invoices.map((invoice) => (
|
||||||
<Link
|
<Link
|
||||||
key={invoice.id}
|
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"
|
className="flex items-center justify-between px-4 py-3 hover:bg-gray-50 transition-colors"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
@@ -147,7 +161,7 @@ export default async function CompanyPage({ params }: { params: Promise<{ id: st
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Badge variant={statusVariants[invoice.status]}>{statusLabels[invoice.status]}</Badge>
|
<Badge variant={statusVariants[invoice.status]}>{statusLabels[invoice.status]}</Badge>
|
||||||
<span className="text-sm font-medium text-gray-900">
|
<span className="text-sm font-medium text-gray-900">
|
||||||
{formatCurrency(Number(invoice.grossTotal))}
|
{formatCurrency(invoice.grossTotal)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -157,7 +171,6 @@ export default async function CompanyPage({ params }: { params: Promise<{ id: st
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Company Info */}
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -166,7 +179,7 @@ export default async function CompanyPage({ params }: { params: Promise<{ id: st
|
|||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500">Bezahlt (gesamt)</p>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500">Rechnungen</p>
|
<p className="text-xs text-gray-500">Rechnungen</p>
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
"use client";
|
import { Link, useNavigate } from "react-router";
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { CompanyForm } from "@/components/company/company-form";
|
import { CompanyForm } from "@/components/company/company-form";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import Link from "next/link";
|
|
||||||
import { ChevronLeft } from "lucide-react";
|
import { ChevronLeft } from "lucide-react";
|
||||||
|
|
||||||
export default function NewCompanyPage() {
|
export default function NewCompanyPage() {
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
async function handleSubmit(data: Record<string, unknown>) {
|
async function handleSubmit(data: Record<string, unknown>) {
|
||||||
const res = await fetch("/api/companies", {
|
const res = await fetch("/api/companies", {
|
||||||
@@ -18,14 +15,14 @@ export default function NewCompanyPage() {
|
|||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const company = await res.json();
|
const company = await res.json();
|
||||||
router.push(`/companies/${company.id}`);
|
navigate(`/companies/${company.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Link
|
<Link
|
||||||
href="/companies"
|
to="/companies"
|
||||||
className="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700 mb-6"
|
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
|
<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 prisma from "@/lib/prisma";
|
||||||
import Link from "next/link";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Building2, Plus, FileText, Users } from "lucide-react";
|
import { Building2, Plus, FileText, Users } from "lucide-react";
|
||||||
|
|
||||||
export default async function CompaniesPage() {
|
export async function loader({ request }: { request: Request }) {
|
||||||
const session = await auth();
|
const user = await requireUser(request);
|
||||||
const companies = await prisma.company.findMany({
|
const companies = await prisma.company.findMany({
|
||||||
where: { userId: session!.user!.id! },
|
where: { userId: user.id },
|
||||||
include: { _count: { select: { invoices: true, customers: true } } },
|
include: { _count: { select: { invoices: true, customers: true } } },
|
||||||
orderBy: { name: "asc" },
|
orderBy: { name: "asc" },
|
||||||
});
|
});
|
||||||
|
return { companies };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CompaniesPage() {
|
||||||
|
const { companies } = useLoaderData<typeof loader>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -21,7 +26,7 @@ export default async function CompaniesPage() {
|
|||||||
<p className="text-gray-500 mt-1">{companies.length} Mandanten verwaltet</p>
|
<p className="text-gray-500 mt-1">{companies.length} Mandanten verwaltet</p>
|
||||||
</div>
|
</div>
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href="/companies/new">
|
<Link to="/companies/new">
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
Mandant anlegen
|
Mandant anlegen
|
||||||
</Link>
|
</Link>
|
||||||
@@ -35,7 +40,7 @@ export default async function CompaniesPage() {
|
|||||||
<h3 className="font-semibold text-gray-700 mb-2">Noch keine Mandanten</h3>
|
<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>
|
<p className="text-gray-500 mb-6 text-sm">Legen Sie Ihren ersten Mandanten an, um loszulegen.</p>
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href="/companies/new">
|
<Link to="/companies/new">
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
Ersten Mandanten anlegen
|
Ersten Mandanten anlegen
|
||||||
</Link>
|
</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">
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||||
{companies.map((company) => (
|
{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">
|
<Card className="hover:shadow-md transition-all hover:border-indigo-200 cursor-pointer h-full">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
@@ -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 prisma from "@/lib/prisma";
|
||||||
import { formatCurrency } from "@/lib/tax";
|
import { formatCurrency } from "@/lib/tax";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Building2, FileText, Euro, TrendingUp } from "lucide-react";
|
import { Building2, FileText, Euro } from "lucide-react";
|
||||||
import Link from "next/link";
|
|
||||||
import { InvoiceStatus } from "@prisma/client";
|
import { InvoiceStatus } from "@prisma/client";
|
||||||
|
|
||||||
export default async function DashboardPage() {
|
export async function loader({ request }: { request: Request }) {
|
||||||
const session = await auth();
|
const user = await requireUser(request);
|
||||||
const userId = session!.user!.id!;
|
const userId = user.id;
|
||||||
|
|
||||||
const [companies, invoiceStats] = await Promise.all([
|
const [companies, invoiceStats, paidTotal, openInvoices] = await Promise.all([
|
||||||
prisma.company.findMany({
|
prisma.company.findMany({
|
||||||
where: { userId },
|
where: { userId },
|
||||||
include: { _count: { select: { invoices: true, customers: true } } },
|
include: { _count: { select: { invoices: true, customers: true } } },
|
||||||
@@ -21,16 +21,25 @@ export default async function DashboardPage() {
|
|||||||
_count: true,
|
_count: true,
|
||||||
_sum: { grossTotal: true },
|
_sum: { grossTotal: true },
|
||||||
}),
|
}),
|
||||||
]);
|
prisma.invoice.aggregate({
|
||||||
|
|
||||||
const paidTotal = await prisma.invoice.aggregate({
|
|
||||||
where: { company: { userId }, status: InvoiceStatus.PAID },
|
where: { company: { userId }, status: InvoiceStatus.PAID },
|
||||||
_sum: { grossTotal: true },
|
_sum: { grossTotal: true },
|
||||||
});
|
}),
|
||||||
|
prisma.invoice.count({
|
||||||
const openInvoices = await prisma.invoice.count({
|
|
||||||
where: { company: { userId }, status: { in: [InvoiceStatus.SENT, InvoiceStatus.DRAFT] } },
|
where: { company: { userId }, status: { in: [InvoiceStatus.SENT, InvoiceStatus.DRAFT] } },
|
||||||
});
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
companies,
|
||||||
|
totalInvoices: invoiceStats._count,
|
||||||
|
paidTotal: Number(paidTotal._sum.grossTotal ?? 0),
|
||||||
|
openInvoices,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DashboardPage() {
|
||||||
|
const { companies, totalInvoices, paidTotal, openInvoices } = useLoaderData<typeof loader>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -39,7 +48,6 @@ export default async function DashboardPage() {
|
|||||||
<p className="text-gray-500 mt-1">Übersicht aller Mandanten und Rechnungen</p>
|
<p className="text-gray-500 mt-1">Übersicht aller Mandanten und Rechnungen</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats */}
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
@@ -62,7 +70,7 @@ export default async function DashboardPage() {
|
|||||||
<FileText className="h-5 w-5 text-blue-600" />
|
<FileText className="h-5 w-5 text-blue-600" />
|
||||||
</div>
|
</div>
|
||||||
<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>
|
<p className="text-sm text-gray-500">Rechnungen gesamt</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,9 +98,7 @@ export default async function DashboardPage() {
|
|||||||
<Euro className="h-5 w-5 text-green-600" />
|
<Euro className="h-5 w-5 text-green-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-2xl font-bold text-gray-900">
|
<p className="text-2xl font-bold text-gray-900">{formatCurrency(paidTotal)}</p>
|
||||||
{formatCurrency(Number(paidTotal._sum.grossTotal ?? 0))}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-500">Bezahlt (brutto)</p>
|
<p className="text-sm text-gray-500">Bezahlt (brutto)</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,14 +106,10 @@ export default async function DashboardPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Companies */}
|
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h2 className="text-lg font-semibold text-gray-900">Mandanten</h2>
|
<h2 className="text-lg font-semibold text-gray-900">Mandanten</h2>
|
||||||
<Link
|
<Link to="/companies" className="text-sm text-indigo-600 hover:text-indigo-700 font-medium">
|
||||||
href="/companies"
|
|
||||||
className="text-sm text-indigo-600 hover:text-indigo-700 font-medium"
|
|
||||||
>
|
|
||||||
Alle anzeigen →
|
Alle anzeigen →
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,7 +120,7 @@ export default async function DashboardPage() {
|
|||||||
<Building2 className="h-12 w-12 text-gray-300 mx-auto mb-4" />
|
<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>
|
<p className="text-gray-500 mb-4">Noch keine Mandanten angelegt.</p>
|
||||||
<Link
|
<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"
|
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
|
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">
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||||
{companies.map((company) => (
|
{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">
|
<Card className="hover:shadow-md transition-shadow cursor-pointer">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-base">{company.name}</CardTitle>
|
<CardTitle className="text-base">{company.name}</CardTitle>
|
||||||
@@ -1,41 +1,32 @@
|
|||||||
"use client";
|
import { Form, useActionData, useNavigation, redirect } from "react-router";
|
||||||
|
import { login, createUserSession, getUserSession } from "@/session.server";
|
||||||
import { useState } from "react";
|
|
||||||
import { signIn } from "next-auth/react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Calculator, AlertCircle } from "lucide-react";
|
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() {
|
export default function LoginPage() {
|
||||||
const router = useRouter();
|
const actionData = useActionData<typeof action>();
|
||||||
const [email, setEmail] = useState("");
|
const navigation = useNavigation();
|
||||||
const [password, setPassword] = useState("");
|
const loading = navigation.state === "submitting";
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-indigo-50 to-blue-50 flex items-center justify-center p-4">
|
<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>
|
<CardDescription>Geben Sie Ihre Zugangsdaten ein</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<Form method="post" className="space-y-4">
|
||||||
{error && (
|
{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">
|
<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" />
|
<AlertCircle className="h-4 w-4 shrink-0" />
|
||||||
{error}
|
{actionData.error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -66,10 +57,9 @@ export default function LoginPage() {
|
|||||||
<Label htmlFor="email">E-Mail</Label>
|
<Label htmlFor="email">E-Mail</Label>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="anna@example.de"
|
placeholder="anna@example.de"
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
required
|
required
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
/>
|
/>
|
||||||
@@ -79,9 +69,8 @@ export default function LoginPage() {
|
|||||||
<Label htmlFor="password">Passwort</Label>
|
<Label htmlFor="password">Passwort</Label>
|
||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
required
|
required
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
/>
|
/>
|
||||||
@@ -90,7 +79,7 @@ export default function LoginPage() {
|
|||||||
<Button type="submit" className="w-full" disabled={loading}>
|
<Button type="submit" className="w-full" disabled={loading}>
|
||||||
{loading ? "Anmelden..." : "Anmelden"}
|
{loading ? "Anmelden..." : "Anmelden"}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { logout } from "@/session.server";
|
||||||
|
|
||||||
|
export async function action({ request }: { request: Request }) {
|
||||||
|
return logout(request);
|
||||||
|
}
|
||||||
@@ -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) },
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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
@@ -1,18 +1,13 @@
|
|||||||
import { defineConfig, globalIgnores } from "eslint/config";
|
import js from "@eslint/js";
|
||||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
||||||
import nextTs from "eslint-config-next/typescript";
|
|
||||||
|
|
||||||
const eslintConfig = defineConfig([
|
export default [
|
||||||
...nextVitals,
|
js.configs.recommended,
|
||||||
...nextTs,
|
{
|
||||||
// Override default ignores of eslint-config-next.
|
ignores: ["build/**", ".react-router/**", "node_modules/**"],
|
||||||
globalIgnores([
|
},
|
||||||
// Default ignores of eslint-config-next:
|
{
|
||||||
".next/**",
|
rules: {
|
||||||
"out/**",
|
"no-unused-vars": "off",
|
||||||
"build/**",
|
},
|
||||||
"next-env.d.ts",
|
},
|
||||||
]),
|
];
|
||||||
]);
|
|
||||||
|
|
||||||
export default eslintConfig;
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { NextConfig } from "next";
|
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
|
||||||
/* config options here */
|
|
||||||
};
|
|
||||||
|
|
||||||
export default nextConfig;
|
|
||||||
Generated
+2247
-3857
File diff suppressed because it is too large
Load Diff
+16
-10
@@ -2,10 +2,12 @@
|
|||||||
"name": "annas-app",
|
"name": "annas-app",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "docker-compose up -d && react-router dev",
|
||||||
"build": "next build",
|
"build": "react-router build",
|
||||||
"start": "next start",
|
"start": "react-router-serve ./build/server/index.js",
|
||||||
|
"typecheck": "react-router typegen && tsc",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"db:migrate": "prisma migrate dev",
|
"db:migrate": "prisma migrate dev",
|
||||||
"db:seed": "ts-node --compiler-options '{\"module\":\"CommonJS\"}' prisma/seed.ts",
|
"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"
|
"seed": "npx ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/prisma-adapter": "^2.11.1",
|
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@prisma/client": "^5.22.0",
|
"@prisma/client": "^5.22.0",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
@@ -26,29 +27,34 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-toast": "^1.2.15",
|
"@radix-ui/react-toast": "^1.2.15",
|
||||||
"@react-pdf/renderer": "^4.3.2",
|
"@react-pdf/renderer": "^4.3.2",
|
||||||
|
"@react-router/node": "^7.13.1",
|
||||||
|
"@react-router/serve": "^7",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"isbot": "^5",
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
"next": "16.1.6",
|
|
||||||
"next-auth": "^5.0.0-beta.30",
|
|
||||||
"prisma": "^5.22.0",
|
"prisma": "^5.22.0",
|
||||||
"react": "19.2.3",
|
"react": "^19",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "^19",
|
||||||
"react-hook-form": "^7.71.2",
|
"react-hook-form": "^7.71.2",
|
||||||
|
"react-router": "^7",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@react-router/dev": "^7.13.1",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"@vitejs/plugin-react": "^4",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.1.6",
|
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5"
|
"typescript": "^5",
|
||||||
|
"vite": "^6",
|
||||||
|
"vite-tsconfig-paths": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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
@@ -7,7 +7,7 @@ async function main() {
|
|||||||
console.log("Seeding database...");
|
console.log("Seeding database...");
|
||||||
|
|
||||||
// Create demo user
|
// Create demo user
|
||||||
const passwordHash = await bcrypt.hash("demo123", 12);
|
const passwordHash = await bcrypt.hash("annas_password", 12);
|
||||||
const user = await prisma.user.upsert({
|
const user = await prisma.user.upsert({
|
||||||
where: { email: "anna@example.de" },
|
where: { email: "anna@example.de" },
|
||||||
update: {},
|
update: {},
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import type { Config } from "@react-router/dev/config";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
ssr: true,
|
||||||
|
} satisfies Config;
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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 });
|
|
||||||
}
|
|
||||||
@@ -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 });
|
|
||||||
}
|
|
||||||
@@ -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 });
|
|
||||||
}
|
|
||||||
@@ -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 |
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -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
Reference in New Issue
Block a user