feat: Initial implementation of Annas Rechnungsmanager

Full-stack German accounting & invoice management web application:

- Multi-company management (Mandantenverwaltung) with full CRUD
- Invoice creation with dynamic line items and automatic tax calculation
- Sequential invoice numbering per company (RE-2024-001 format)
- §14 UStG compliant PDF invoice generation via @react-pdf/renderer
- Customer management (Kundenverwaltung) per company
- Tax reports: quarterly USt-Voranmeldung and monthly revenue overview
- Email/password authentication via NextAuth.js v5
- Responsive, modern UI with Tailwind CSS and custom shadcn/ui components
- Prisma v5 ORM with MySQL/MariaDB schema + demo seed data

Stack: Next.js 14 (App Router) · TypeScript · Prisma/MySQL · NextAuth.js

https://claude.ai/code/session_01FN53KKxo5ebrGwqFhxzkT9
This commit is contained in:
Claude
2026-03-07 17:27:57 +00:00
commit 44e79e657f
65 changed files with 12722 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
passwordHash String
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
companies Company[]
@@map("users")
}
model Company {
id String @id @default(cuid())
name String
legalForm String?
taxId String?
vatId String?
address String
zip String
city String
country String @default("DE")
email String?
phone String?
website String?
bankIban String?
bankBic String?
bankName String?
invoicePrefix String @default("RE")
invoiceSequence Int @default(0)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
customers Customer[]
invoices Invoice[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("companies")
}
model Customer {
id String @id @default(cuid())
companyId String
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
name String
vatId String?
taxId String?
address String
zip String
city String
country String @default("DE")
email String?
phone String?
invoices Invoice[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("customers")
}
model Invoice {
id String @id @default(cuid())
number String
companyId String
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
customerId String
customer Customer @relation(fields: [customerId], references: [id])
issueDate DateTime
deliveryDate DateTime?
dueDate DateTime
status InvoiceStatus @default(DRAFT)
notes String? @db.Text
items InvoiceItem[]
netTotal Decimal @db.Decimal(10, 2)
taxTotal Decimal @db.Decimal(10, 2)
grossTotal Decimal @db.Decimal(10, 2)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([companyId, number])
@@map("invoices")
}
enum InvoiceStatus {
DRAFT
SENT
PAID
CANCELLED
}
model InvoiceItem {
id String @id @default(cuid())
invoiceId String
invoice Invoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
position Int
description String @db.Text
quantity Decimal @db.Decimal(10, 3)
unit String?
unitPrice Decimal @db.Decimal(10, 2)
taxRate Decimal @db.Decimal(5, 2)
netAmount Decimal @db.Decimal(10, 2)
taxAmount Decimal @db.Decimal(10, 2)
grossAmount Decimal @db.Decimal(10, 2)
@@map("invoice_items")
}
+112
View File
@@ -0,0 +1,112 @@
import { PrismaClient } from "@prisma/client";
import bcrypt from "bcryptjs";
const prisma = new PrismaClient();
async function main() {
console.log("Seeding database...");
// Create demo user
const passwordHash = await bcrypt.hash("demo123", 12);
const user = await prisma.user.upsert({
where: { email: "anna@example.de" },
update: {},
create: {
email: "anna@example.de",
passwordHash,
name: "Anna Musterfrau",
},
});
console.log(`✓ User created: ${user.email}`);
// Create demo company
const company = await prisma.company.upsert({
where: { id: "demo-company-1" },
update: {},
create: {
id: "demo-company-1",
name: "Muster GmbH",
legalForm: "GmbH",
taxId: "123/456/78901",
vatId: "DE123456789",
address: "Musterstraße 1",
zip: "10115",
city: "Berlin",
email: "info@muster-gmbh.de",
phone: "+49 30 12345678",
bankName: "Musterbank",
bankIban: "DE89 3704 0044 0532 0130 00",
bankBic: "COBADEFFXXX",
invoicePrefix: "RE",
userId: user.id,
},
});
console.log(`✓ Company created: ${company.name}`);
// Create demo customer
const customer = await prisma.customer.upsert({
where: { id: "demo-customer-1" },
update: {},
create: {
id: "demo-customer-1",
companyId: company.id,
name: "Beispiel AG",
vatId: "DE987654321",
address: "Beispielweg 5",
zip: "20095",
city: "Hamburg",
email: "kontakt@beispiel-ag.de",
},
});
console.log(`✓ Customer created: ${customer.name}`);
// Create demo invoice
const invoice = await prisma.invoice.create({
data: {
number: "RE-2024-001",
companyId: company.id,
customerId: customer.id,
issueDate: new Date("2024-01-15"),
deliveryDate: new Date("2024-01-15"),
dueDate: new Date("2024-02-14"),
status: "SENT",
netTotal: 1000.0,
taxTotal: 190.0,
grossTotal: 1190.0,
items: {
create: [
{
position: 1,
description: "Buchhaltungsleistungen Januar 2024",
quantity: 10,
unit: "h",
unitPrice: 100.0,
taxRate: 19.0,
netAmount: 1000.0,
taxAmount: 190.0,
grossAmount: 1190.0,
},
],
},
},
});
console.log(`✓ Invoice created: ${invoice.number}`);
// Update company sequence
await prisma.company.update({
where: { id: company.id },
data: { invoiceSequence: 1 },
});
console.log("\n✅ Seed complete!");
console.log("Login: anna@example.de / demo123");
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});