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 = { QUOTE: 'ANG', ORDER_CONFIRMATION: 'AB', DELIVERY_NOTE: 'LS', INVOICE: 'RE', CREDIT_NOTE: 'RK' }; const NUMBER_FIELDS: Record = { 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 { 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 }); } }