44e79e657f
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
75 lines
2.2 KiB
TypeScript
75 lines
2.2 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { auth } from "@/lib/auth";
|
|
import prisma from "@/lib/prisma";
|
|
import { generateInvoiceNumber } from "@/lib/invoice-number";
|
|
import { z } from "zod";
|
|
|
|
const itemSchema = z.object({
|
|
position: z.number().int(),
|
|
description: z.string().min(1),
|
|
quantity: z.number().positive(),
|
|
unit: z.string().optional(),
|
|
unitPrice: z.number(),
|
|
taxRate: z.number(),
|
|
netAmount: z.number(),
|
|
taxAmount: z.number(),
|
|
grossAmount: z.number(),
|
|
});
|
|
|
|
const invoiceSchema = z.object({
|
|
companyId: z.string().min(1),
|
|
customerId: z.string().min(1),
|
|
issueDate: z.string(),
|
|
deliveryDate: z.string().optional(),
|
|
dueDate: z.string(),
|
|
notes: z.string().optional(),
|
|
items: z.array(itemSchema).min(1),
|
|
netTotal: z.number(),
|
|
taxTotal: z.number(),
|
|
grossTotal: z.number(),
|
|
});
|
|
|
|
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 = invoiceSchema.safeParse(body);
|
|
if (!parsed.success) return NextResponse.json({ error: parsed.error.issues }, { status: 400 });
|
|
|
|
const { items, companyId, ...invoiceData } = parsed.data;
|
|
|
|
// Verify company belongs to user
|
|
const company = await prisma.company.findFirst({
|
|
where: { id: companyId, userId: session.user.id },
|
|
});
|
|
if (!company) return NextResponse.json({ error: "Company not found" }, { status: 404 });
|
|
|
|
const number = await generateInvoiceNumber(companyId);
|
|
|
|
const invoice = await prisma.invoice.create({
|
|
data: {
|
|
...invoiceData,
|
|
number,
|
|
companyId,
|
|
issueDate: new Date(invoiceData.issueDate),
|
|
deliveryDate: invoiceData.deliveryDate ? new Date(invoiceData.deliveryDate) : null,
|
|
dueDate: new Date(invoiceData.dueDate),
|
|
items: {
|
|
create: items.map((item) => ({
|
|
...item,
|
|
quantity: item.quantity,
|
|
unitPrice: item.unitPrice,
|
|
taxRate: item.taxRate,
|
|
netAmount: item.netAmount,
|
|
taxAmount: item.taxAmount,
|
|
grossAmount: item.grossAmount,
|
|
})),
|
|
},
|
|
},
|
|
include: { items: true, customer: true },
|
|
});
|
|
|
|
return NextResponse.json(invoice, { status: 201 });
|
|
}
|