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:
@@ -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
@@ -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();
|
||||
});
|
||||
Reference in New Issue
Block a user