Initial commit - ERP System

This commit is contained in:
root
2026-05-20 18:58:23 +00:00
commit e174936997
2697 changed files with 1628427 additions and 0 deletions
+231
View File
@@ -0,0 +1,231 @@
import { NextResponse } from 'next/server';
import prisma from '../../../../lib/prisma';
import { getServerSession } from "next-auth/next";
import { authOptions } from "../../auth/[...nextauth]/route";
const PREFIXES: Record<string, string> = {
QUOTE: 'ANG', ORDER_CONFIRMATION: 'AB', DELIVERY_NOTE: 'LS', INVOICE: 'RE', CREDIT_NOTE: 'RK'
};
const NUMBER_FIELDS: Record<string, string> = {
QUOTE: 'nextQuoteNumber', ORDER_CONFIRMATION: 'nextOrderNumber',
DELIVERY_NOTE: 'nextDeliveryNumber', INVOICE: 'nextInvoiceNumber', CREDIT_NOTE: 'nextCreditNoteNumber'
};
async function generateNumber(type: string) {
const settings = await prisma.systemSettings.findFirst();
if (!settings) throw new Error('SystemSettings not found');
const year = new Date().getFullYear();
const prefix = PREFIXES[type] || 'DOC';
const field = NUMBER_FIELDS[type];
const num = (settings as any)[field] || 1;
await prisma.systemSettings.update({ where: { id: settings.id }, data: { [field]: num + 1 } });
return `${prefix}-${year}-${num.toString().padStart(4, '0')}`;
}
// Set status to ARCHIVED while preserving original status in previousStatus
async function archiveDoc(docId: number) {
const doc = await prisma.salesDocument.findUnique({ where: { id: docId }, select: { status: true } });
if (!doc || doc.status === 'ARCHIVED' || doc.status === 'CANCELLED') return;
await prisma.salesDocument.update({
where: { id: docId },
data: { previousStatus: doc.status, status: 'ARCHIVED' }
});
}
// Walk the chain back via sourceDocumentId and collect all related doc IDs
async function getChainIds(docId: number): Promise<number[]> {
const ids: number[] = [];
let currentId: number | null = docId;
while (currentId) {
const doc = await prisma.salesDocument.findUnique({ where: { id: currentId }, select: { sourceDocumentId: true } });
if (doc?.sourceDocumentId) {
ids.push(doc.sourceDocumentId);
currentId = doc.sourceDocumentId;
} else {
currentId = null;
}
}
return ids;
}
export async function GET(request: Request, context: { params: Promise<{ id: string }> }) {
try {
const { id } = await context.params;
const doc = await prisma.salesDocument.findUnique({
where: { id: parseInt(id) },
include: {
customer: true,
items: { include: { product: true } },
createdBy: { select: { firstName: true, lastName: true } }
}
});
if (!doc) return NextResponse.json({ error: 'Nicht gefunden' }, { status: 404 });
return NextResponse.json(doc);
} catch (error) {
return NextResponse.json({ error: 'Ladefehler' }, { status: 500 });
}
}
export async function PUT(request: Request, context: { params: Promise<{ id: string }> }) {
const session = await getServerSession(authOptions);
const perms = (session?.user as any)?.permissions || [];
if (!perms.includes('SALES_MANAGE')) return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 });
try {
const { id } = await context.params;
const body = await request.json();
const docId = parseInt(id);
// Handle signature (not for invoices/credit notes)
if (body.signatureData) {
const doc = await prisma.salesDocument.findUnique({ where: { id: docId } });
if (doc?.type === 'INVOICE' || doc?.type === 'CREDIT_NOTE') {
return NextResponse.json({ error: 'Kann nicht unterschrieben werden' }, { status: 400 });
}
const updated = await prisma.salesDocument.update({
where: { id: docId },
data: { signatureData: body.signatureData, signedAt: new Date(), status: 'ACCEPTED' }
});
return NextResponse.json(updated);
}
// Handle "create follow-up document" action
if (body.action === 'CREATE_FOLLOWUP') {
const doc = await prisma.salesDocument.findUnique({ where: { id: docId }, include: { items: true } });
if (!doc) return NextResponse.json({ error: 'Not found' }, { status: 404 });
let newType = '';
if (doc.type === 'ORDER_CONFIRMATION') newType = 'DELIVERY_NOTE';
else if (doc.type === 'DELIVERY_NOTE') newType = 'INVOICE';
else if (doc.type === 'INVOICE') newType = 'CREDIT_NOTE';
else return NextResponse.json({ error: 'Kein Folgebeleg möglich' }, { status: 400 });
const number = await generateNumber(newType);
const followUp = await prisma.salesDocument.create({
data: {
type: newType as any,
number,
customerId: doc.customerId,
createdById: (session?.user as any)?.id || null,
sourceDocumentId: doc.id,
subtotal: doc.subtotal, taxAmount: doc.taxAmount, total: doc.total,
notes: newType === 'CREDIT_NOTE' ? `Rechnungskorrektur zu ${doc.number}` : doc.notes,
items: {
create: doc.items.map(i => ({
description: i.description, quantity: i.quantity, unitPrice: i.unitPrice,
taxRate: i.taxRate, total: i.total, productId: i.productId
}))
}
},
include: { items: true }
});
// CREDIT_NOTE → cancel the original invoice
if (newType === 'CREDIT_NOTE') {
await prisma.salesDocument.update({
where: { id: doc.id },
data: { previousStatus: doc.status, status: 'CANCELLED' }
});
} else {
// Archive the source document
await archiveDoc(doc.id);
}
// If creating an INVOICE → also archive all earlier docs in the chain
if (newType === 'INVOICE') {
const chainIds = await getChainIds(followUp.id);
for (const cid of chainIds) { await archiveDoc(cid); }
}
return NextResponse.json(followUp, { status: 201 });
}
// Handle status changes
if (body.status) {
const doc = await prisma.salesDocument.findUnique({
where: { id: docId },
include: { items: { include: { product: true } } }
});
if (!doc) return NextResponse.json({ error: 'Not found' }, { status: 404 });
// QUOTE accepted → reserve stock + auto-create AB (SENT) + archive quote
if (doc.type === 'QUOTE' && body.status === 'ACCEPTED') {
for (const item of doc.items) {
if (item.productId && item.product?.trackStock) {
await prisma.product.update({
where: { id: item.productId },
data: { reservedStock: { increment: Math.ceil(item.quantity) } }
});
}
}
// Auto-create AB with status SENT
const abNumber = await generateNumber('ORDER_CONFIRMATION');
const ab = await prisma.salesDocument.create({
data: {
type: 'ORDER_CONFIRMATION',
number: abNumber,
status: 'SENT',
customerId: doc.customerId,
createdById: (session?.user as any)?.id || null,
sourceDocumentId: doc.id,
subtotal: doc.subtotal, taxAmount: doc.taxAmount, total: doc.total,
notes: doc.notes,
items: {
create: doc.items.map(i => ({
description: i.description, quantity: i.quantity, unitPrice: i.unitPrice,
taxRate: i.taxRate, total: i.total, productId: i.productId
}))
}
}
});
// Archive the quote (preserving ACCEPTED as previousStatus)
await prisma.salesDocument.update({
where: { id: docId },
data: { previousStatus: 'ACCEPTED', status: 'ARCHIVED' }
});
return NextResponse.json({ ...doc, status: 'ARCHIVED', followUpId: ab.id, followUpNumber: ab.number });
}
// DELIVERY_NOTE delivered → reduce stock
if (doc.type === 'DELIVERY_NOTE' && body.status === 'DELIVERED') {
for (const item of doc.items) {
if (item.productId && item.product?.trackStock) {
await prisma.product.update({
where: { id: item.productId },
data: {
stock: { decrement: Math.ceil(item.quantity) },
reservedStock: { decrement: Math.ceil(item.quantity) }
}
});
}
}
}
// INVOICE paid → mark as paid then archive
if (doc.type === 'INVOICE' && body.status === 'PAID') {
await prisma.salesDocument.update({
where: { id: docId },
data: { previousStatus: 'PAID', status: 'ARCHIVED' }
});
return NextResponse.json({ ...doc, status: 'ARCHIVED', previousStatus: 'PAID' });
}
}
const updated = await prisma.salesDocument.update({
where: { id: docId },
data: {
status: body.status || undefined,
notes: body.notes !== undefined ? body.notes : undefined,
},
include: { items: true, customer: true }
});
return NextResponse.json(updated);
} catch (error) {
console.error(error);
return NextResponse.json({ error: 'Fehler' }, { status: 500 });
}
}