232 lines
8.8 KiB
TypeScript
232 lines
8.8 KiB
TypeScript
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 });
|
|
}
|
|
}
|