Initial commit - ERP System
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user