diff --git a/.zscripts/dev.pid b/.zscripts/dev.pid old mode 100644 new mode 100755 index 4524e19..2b4ff56 --- a/.zscripts/dev.pid +++ b/.zscripts/dev.pid @@ -1 +1 @@ -984 +1114 diff --git a/aos-shots/01-dashboard.png b/aos-shots/01-dashboard.png old mode 100644 new mode 100755 diff --git a/aos-shots/02-crm.png b/aos-shots/02-crm.png old mode 100644 new mode 100755 diff --git a/aos-shots/03-leads.png b/aos-shots/03-leads.png old mode 100644 new mode 100755 diff --git a/aos-shots/04-projects.png b/aos-shots/04-projects.png old mode 100644 new mode 100755 diff --git a/aos-shots/05-licenses.png b/aos-shots/05-licenses.png old mode 100644 new mode 100755 diff --git a/aos-shots/06-finance.png b/aos-shots/06-finance.png old mode 100644 new mode 100755 diff --git a/aos-shots/07-settings.png b/aos-shots/07-settings.png old mode 100644 new mode 100755 diff --git a/aos-shots/08-lead-explain.png b/aos-shots/08-lead-explain.png old mode 100644 new mode 100755 diff --git a/aos-shots/09-mobile-dashboard.png b/aos-shots/09-mobile-dashboard.png old mode 100644 new mode 100755 diff --git a/aos-shots/10-mobile-crm.png b/aos-shots/10-mobile-crm.png old mode 100644 new mode 100755 diff --git a/aos-shots/v10-estimate-detail.png b/aos-shots/v10-estimate-detail.png old mode 100644 new mode 100755 diff --git a/aos-shots/v10-estimates.png b/aos-shots/v10-estimates.png old mode 100644 new mode 100755 diff --git a/aos-shots/v10-support-summarized.png b/aos-shots/v10-support-summarized.png old mode 100644 new mode 100755 diff --git a/aos-shots/v10-support.png b/aos-shots/v10-support.png old mode 100644 new mode 100755 diff --git a/aos-shots/v11-reporting-mrr.png b/aos-shots/v11-reporting-mrr.png old mode 100644 new mode 100755 diff --git a/aos-shots/v11-reporting-utilization.png b/aos-shots/v11-reporting-utilization.png old mode 100644 new mode 100755 diff --git a/aos-shots/v11-reporting.png b/aos-shots/v11-reporting.png old mode 100644 new mode 100755 diff --git a/aos-shots/v11-wizard-step1.png b/aos-shots/v11-wizard-step1.png old mode 100644 new mode 100755 diff --git a/aos-shots/v11-wizard-step3-modules.png b/aos-shots/v11-wizard-step3-modules.png old mode 100644 new mode 100755 diff --git a/aos-shots/v11-wizard-step4-mail.png b/aos-shots/v11-wizard-step4-mail.png old mode 100644 new mode 100755 diff --git a/aos-shots/v12-wizard.png b/aos-shots/v12-wizard.png old mode 100644 new mode 100755 diff --git a/aos-shots/v13-accounting-bank.png b/aos-shots/v13-accounting-bank.png old mode 100644 new mode 100755 diff --git a/aos-shots/v13-accounting-dunning.png b/aos-shots/v13-accounting-dunning.png old mode 100644 new mode 100755 diff --git a/aos-shots/v13-accounting-eur.png b/aos-shots/v13-accounting-eur.png old mode 100644 new mode 100755 diff --git a/aos-shots/v13-accounting-vat.png b/aos-shots/v13-accounting-vat.png old mode 100644 new mode 100755 diff --git a/aos-shots/v14-mail-real-test.png b/aos-shots/v14-mail-real-test.png old mode 100644 new mode 100755 diff --git a/aos-shots/v14-vat-real-xml.png b/aos-shots/v14-vat-real-xml.png old mode 100644 new mode 100755 diff --git a/aos-shots/v15-api-docs.png b/aos-shots/v15-api-docs.png old mode 100644 new mode 100755 diff --git a/aos-shots/v15-api-tokens.png b/aos-shots/v15-api-tokens.png old mode 100644 new mode 100755 diff --git a/aos-shots/v16-portal-approval-dialog.png b/aos-shots/v16-portal-approval-dialog.png old mode 100644 new mode 100755 diff --git a/aos-shots/v16-portal-approval-done.png b/aos-shots/v16-portal-approval-done.png old mode 100644 new mode 100755 diff --git a/aos-shots/v16-portal-approvals.png b/aos-shots/v16-portal-approvals.png old mode 100644 new mode 100755 diff --git a/aos-shots/v16-portal-invoices.png b/aos-shots/v16-portal-invoices.png old mode 100644 new mode 100755 diff --git a/aos-shots/v16-portal-login.png b/aos-shots/v16-portal-login.png old mode 100644 new mode 100755 diff --git a/aos-shots/v16-portal-overview.png b/aos-shots/v16-portal-overview.png old mode 100644 new mode 100755 diff --git a/aos-shots/v17-de.png b/aos-shots/v17-de.png old mode 100644 new mode 100755 diff --git a/aos-shots/v17-debug1.png b/aos-shots/v17-debug1.png old mode 100644 new mode 100755 diff --git a/aos-shots/v17-dropdown-open.png b/aos-shots/v17-dropdown-open.png old mode 100644 new mode 100755 diff --git a/aos-shots/v17-en-final.png b/aos-shots/v17-en-final.png old mode 100644 new mode 100755 diff --git a/aos-shots/v17-en-persisted.png b/aos-shots/v17-en-persisted.png old mode 100644 new mode 100755 diff --git a/aos-shots/v17-en-real-final.png b/aos-shots/v17-en-real-final.png old mode 100644 new mode 100755 diff --git a/aos-shots/v17-en-real.png b/aos-shots/v17-en-real.png old mode 100644 new mode 100755 diff --git a/aos-shots/v17-en-verified.png b/aos-shots/v17-en-verified.png old mode 100644 new mode 100755 diff --git a/aos-shots/v17-en.png b/aos-shots/v17-en.png old mode 100644 new mode 100755 diff --git a/aos-shots/v17-locale-dropdown.png b/aos-shots/v17-locale-dropdown.png old mode 100644 new mode 100755 diff --git a/aos-shots/v17-settings.png b/aos-shots/v17-settings.png old mode 100644 new mode 100755 diff --git a/aos-shots/v17-tr-persisted.png b/aos-shots/v17-tr-persisted.png old mode 100644 new mode 100755 diff --git a/aos-shots/v17-tr.png b/aos-shots/v17-tr.png old mode 100644 new mode 100755 diff --git a/aos-shots/v18-kb-article.png b/aos-shots/v18-kb-article.png old mode 100644 new mode 100755 diff --git a/aos-shots/v18-kb.png b/aos-shots/v18-kb.png old mode 100644 new mode 100755 diff --git a/aos-shots/v18-notifications.png b/aos-shots/v18-notifications.png old mode 100644 new mode 100755 diff --git a/aos-shots/v19-calendar-next.png b/aos-shots/v19-calendar-next.png old mode 100644 new mode 100755 diff --git a/aos-shots/v19-calendar.png b/aos-shots/v19-calendar.png old mode 100644 new mode 100755 diff --git a/aos-shots/v2-CRM.png b/aos-shots/v2-CRM.png old mode 100644 new mode 100755 diff --git a/aos-shots/v2-Dashboard.png b/aos-shots/v2-Dashboard.png old mode 100644 new mode 100755 diff --git a/aos-shots/v2-Erweiterungen.png b/aos-shots/v2-Erweiterungen.png old mode 100644 new mode 100755 diff --git a/aos-shots/v2-Finanzen.png b/aos-shots/v2-Finanzen.png old mode 100644 new mode 100755 diff --git a/aos-shots/v2-Lizenzen.png b/aos-shots/v2-Lizenzen.png old mode 100644 new mode 100755 diff --git a/aos-shots/v2-Projekte.png b/aos-shots/v2-Projekte.png old mode 100644 new mode 100755 diff --git a/aos-shots/v2-ai-explain.png b/aos-shots/v2-ai-explain.png old mode 100644 new mode 100755 diff --git a/aos-shots/v2-leads-initial.png b/aos-shots/v2-leads-initial.png old mode 100644 new mode 100755 diff --git a/aos-shots/v20-onboarding-dismissed.png b/aos-shots/v20-onboarding-dismissed.png old mode 100644 new mode 100755 diff --git a/aos-shots/v20-onboarding-last.png b/aos-shots/v20-onboarding-last.png old mode 100644 new mode 100755 diff --git a/aos-shots/v20-onboarding-step1.png b/aos-shots/v20-onboarding-step1.png old mode 100644 new mode 100755 diff --git a/aos-shots/v20-onboarding-step2.png b/aos-shots/v20-onboarding-step2.png old mode 100644 new mode 100755 diff --git a/aos-shots/v21-dark-crm.png b/aos-shots/v21-dark-crm.png old mode 100644 new mode 100755 diff --git a/aos-shots/v21-dark-dashboard.png b/aos-shots/v21-dark-dashboard.png old mode 100644 new mode 100755 diff --git a/aos-shots/v21-dark-finance.png b/aos-shots/v21-dark-finance.png old mode 100644 new mode 100755 diff --git a/aos-shots/v21-dark-portal.png b/aos-shots/v21-dark-portal.png old mode 100644 new mode 100755 diff --git a/aos-shots/v22-mobile-calendar.png b/aos-shots/v22-mobile-calendar.png old mode 100644 new mode 100755 diff --git a/aos-shots/v22-mobile-dashboard.png b/aos-shots/v22-mobile-dashboard.png old mode 100644 new mode 100755 diff --git a/aos-shots/v22-mobile-leads.png b/aos-shots/v22-mobile-leads.png old mode 100644 new mode 100755 diff --git a/aos-shots/v22-mobile-menu.png b/aos-shots/v22-mobile-menu.png old mode 100644 new mode 100755 diff --git a/aos-shots/v22-mobile-projects.png b/aos-shots/v22-mobile-projects.png old mode 100644 new mode 100755 diff --git a/aos-shots/v22-mobile-search.png b/aos-shots/v22-mobile-search.png old mode 100644 new mode 100755 diff --git a/aos-shots/v3-audit.png b/aos-shots/v3-audit.png old mode 100644 new mode 100755 diff --git a/aos-shots/v3-company-drawer.png b/aos-shots/v3-company-drawer.png old mode 100644 new mode 100755 diff --git a/aos-shots/v3-invoice-dialog.png b/aos-shots/v3-invoice-dialog.png old mode 100644 new mode 100755 diff --git a/aos-shots/v3-projects-task.png b/aos-shots/v3-projects-task.png old mode 100644 new mode 100755 diff --git a/aos-shots/v4-audit.png b/aos-shots/v4-audit.png old mode 100644 new mode 100755 diff --git a/aos-shots/v4-company-drawer.png b/aos-shots/v4-company-drawer.png old mode 100644 new mode 100755 diff --git a/aos-shots/v4-create-deal.png b/aos-shots/v4-create-deal.png old mode 100644 new mode 100755 diff --git a/aos-shots/v4-invoice-dialog.png b/aos-shots/v4-invoice-dialog.png old mode 100644 new mode 100755 diff --git a/aos-shots/v4-lead-approved.png b/aos-shots/v4-lead-approved.png old mode 100644 new mode 100755 diff --git a/aos-shots/v4-lead-converted.png b/aos-shots/v4-lead-converted.png old mode 100644 new mode 100755 diff --git a/aos-shots/v4-projects-task.png b/aos-shots/v4-projects-task.png old mode 100644 new mode 100755 diff --git a/aos-shots/v4-projects-time.png b/aos-shots/v4-projects-time.png old mode 100644 new mode 100755 diff --git a/aos-shots/v5-company-drawer.png b/aos-shots/v5-company-drawer.png old mode 100644 new mode 100755 diff --git a/aos-shots/v6-audit-detail.png b/aos-shots/v6-audit-detail.png old mode 100644 new mode 100755 diff --git a/aos-shots/v6-audit.png b/aos-shots/v6-audit.png old mode 100644 new mode 100755 diff --git a/aos-shots/v6-command-search.png b/aos-shots/v6-command-search.png old mode 100644 new mode 100755 diff --git a/aos-shots/v6-company-drawer.png b/aos-shots/v6-company-drawer.png old mode 100644 new mode 100755 diff --git a/aos-shots/v6-create-deal-done.png b/aos-shots/v6-create-deal-done.png old mode 100644 new mode 100755 diff --git a/aos-shots/v6-create-deal-filled.png b/aos-shots/v6-create-deal-filled.png old mode 100644 new mode 100755 diff --git a/aos-shots/v6-create-deal.png b/aos-shots/v6-create-deal.png old mode 100644 new mode 100755 diff --git a/aos-shots/v6-invoice-dialog.png b/aos-shots/v6-invoice-dialog.png old mode 100644 new mode 100755 diff --git a/aos-shots/v6-lead-approved.png b/aos-shots/v6-lead-approved.png old mode 100644 new mode 100755 diff --git a/aos-shots/v6-lead-converted.png b/aos-shots/v6-lead-converted.png old mode 100644 new mode 100755 diff --git a/aos-shots/v6-projects-task.png b/aos-shots/v6-projects-task.png old mode 100644 new mode 100755 diff --git a/aos-shots/v6-projects-time.png b/aos-shots/v6-projects-time.png old mode 100644 new mode 100755 diff --git a/aos-shots/v7-palette-open.png b/aos-shots/v7-palette-open.png old mode 100644 new mode 100755 diff --git a/aos-shots/v7-search-results.png b/aos-shots/v7-search-results.png old mode 100644 new mode 100755 diff --git a/aos-shots/v8-search-bauer.png b/aos-shots/v8-search-bauer.png old mode 100644 new mode 100755 diff --git a/aos-shots/v9-contract-detail.png b/aos-shots/v9-contract-detail.png old mode 100644 new mode 100755 diff --git a/aos-shots/v9-contracts.png b/aos-shots/v9-contracts.png old mode 100644 new mode 100755 diff --git a/aos-shots/v9-expenses-report.png b/aos-shots/v9-expenses-report.png old mode 100644 new mode 100755 diff --git a/aos-shots/v9-expenses.png b/aos-shots/v9-expenses.png old mode 100644 new mode 100755 diff --git a/bun.lock b/bun.lock old mode 100644 new mode 100755 diff --git a/data/files/7d/7dc3bd0691230ae41dbbf62fa408b3f1b913561de2ff77ff4772e704580fc7a6 b/data/files/7d/7dc3bd0691230ae41dbbf62fa408b3f1b913561de2ff77ff4772e704580fc7a6 new file mode 100644 index 0000000..757877f --- /dev/null +++ b/data/files/7d/7dc3bd0691230ae41dbbf62fa408b3f1b913561de2ff77ff4772e704580fc7a6 @@ -0,0 +1,2 @@ +%PDF-1.4 +fake content diff --git a/db/custom.db b/db/custom.db old mode 100755 new mode 100644 index 8639c57..eba0676 Binary files a/db/custom.db and b/db/custom.db differ diff --git a/src/app/api/aos/belege/upload/route.ts b/src/app/api/aos/belege/upload/route.ts new file mode 100644 index 0000000..cdc7fe3 --- /dev/null +++ b/src/app/api/aos/belege/upload/route.ts @@ -0,0 +1,103 @@ +import { NextResponse } from 'next/server' +import { db } from '@/lib/db' +import { storeFile, detectContentType } from '@/lib/storage' +import { z } from 'zod' +import { checkRateLimit } from '@/lib/rate-limit' + +export const dynamic = 'force-dynamic' +export const maxDuration = 30 + +// POST /api/aos/belege/upload +// Upload a receipt file with content-addressed storage (PRD §5.5 + §5.7 Belege). +// Accepts multipart/form-data with file + metadata. +export async function POST(req: Request) { + try { + const rateLimited = checkRateLimit(req, 'public') + if (rateLimited) return rateLimited + + const formData = await req.formData() + const file = formData.get('file') as File | null + const type = formData.get('type') as string | null + const vendor = formData.get('vendor') as string | null + const invoiceNumber = formData.get('invoiceNumber') as string | null + const amountCentsStr = formData.get('amountCents') as string | null + const bookingDateStr = formData.get('bookingDate') as string | null + + if (!file) { + return NextResponse.json({ error: 'file required (multipart/form-data, field name: file)' }, { status: 400 }) + } + + // Read file content + const arrayBuffer = await file.arrayBuffer() + const buffer = Buffer.from(arrayBuffer) + + // Detect content type from magic bytes + const detectedType = detectContentType(buffer) + const fileName = file.name + const fileType = file.type || detectedType + const fileSizeBytes = buffer.length + + // Store file (content-addressed, dedup, atomic write) + const stored = await storeFile(buffer, fileType) + + // Check for duplicate (same hash = same file already registered as Beleg) + const existing = await db.beleg.findUnique({ where: { fileHash: stored.hash } }) + if (existing) { + return NextResponse.json({ + beleg: existing, + duplicate: true, + message: 'File already registered (same SHA-256 hash)', + }, { status: 409 }) + } + + // Create Beleg record + const count = await db.beleg.count() + const number = `BEL-2026-${String(count + 1).padStart(4, '0')}` + const belegType = type || 'sonstiges' + + // GoBD retention: 8 years for invoices, 6 for business letters + const retentionUntil = new Date() + retentionUntil.setFullYear(retentionUntil.getFullYear() + (belegType.includes('rechnung') ? 8 : 6)) + + const beleg = await db.beleg.create({ + data: { + number, + type: belegType, + fileHash: stored.hash, + fileName, + fileType, + fileSizeBytes, + vendor: vendor ?? null, + invoiceNumber: invoiceNumber ?? null, + amountCents: amountCentsStr ? parseInt(amountCentsStr) : null, + bookingDate: bookingDateStr ? new Date(bookingDateStr) : null, + retentionUntil, + }, + }) + + await db.auditLog.create({ + data: { + actor: 'nora@nordlicht.digital', + action: 'beleg.upload', + entity: 'Beleg', + entityId: beleg.id, + after: JSON.stringify({ number, type: belegType, fileName, hash: stored.hash.slice(0, 16) + '…', sizeBytes: fileSizeBytes }), + hash: 'sha256:' + Math.random().toString(36).slice(2), + }, + }) + + return NextResponse.json({ + beleg, + storage: { + hash: stored.hash, + sizeBytes: stored.size, + contentType: stored.contentType, + detectedType, + deduped: stored.size !== fileSizeBytes, // if size differs, existing file was returned + }, + }) + } catch (err) { + const msg = err instanceof Error ? err.message : 'internal server error' + return NextResponse.json({ error: msg }, { status: 500 }) + } +} diff --git a/src/app/api/aos/crm/mail-sync/route.ts b/src/app/api/aos/crm/mail-sync/route.ts old mode 100644 new mode 100755 diff --git a/src/app/api/aos/invoices/[id]/pdf/route.ts b/src/app/api/aos/invoices/[id]/pdf/route.ts new file mode 100644 index 0000000..81a1af0 --- /dev/null +++ b/src/app/api/aos/invoices/[id]/pdf/route.ts @@ -0,0 +1,165 @@ +import { NextResponse } from 'next/server' +import { db } from '@/lib/db' + +export const dynamic = 'force-dynamic' +export const maxDuration = 15 + +// GET /api/aos/invoices/[id]/pdf +// Generates a real PDF invoice (German layout) using HTML → print-ready format. +// Returns the invoice as an HTML page that browsers can "Print to PDF". +// In production, this would use Typst (PRD §5.2) for server-side PDF generation. +export async function GET( + _req: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params + + const invoice = await db.invoice.findUnique({ + where: { id }, + include: { company: true, lines: true }, + }) + + if (!invoice) { + return NextResponse.json({ error: 'invoice not found' }, { status: 404 }) + } + + // Get agency settings for the invoice header + const settings = await db.setting.findMany() + const getSetting = (key: string) => settings.find((s) => s.key === key)?.value ?? '' + + const agencyName = getSetting('company.name') || 'Nordlicht Digital GmbH' + const agencyLegalForm = getSetting('company.legalForm') || 'GmbH' + const agencyVatId = getSetting('company.vatId') || '' + const agencyRegisterCourt = getSetting('company.registerCourt') || '' + const agencyRegisterNumber = getSetting('company.registerNumber') || '' + const agencyCity = getSetting('company.city') || '' + const agencyBank = getSetting('company.bank') || '' + const agencyBic = getSetting('company.bic') || '' + + // Generate German invoice HTML (print-ready) + const html = generateInvoiceHTML({ + invoice, + agency: { name: agencyName, legalForm: agencyLegalForm, vatId: agencyVatId, registerCourt: agencyRegisterCourt, registerNumber: agencyRegisterNumber, city: agencyCity, bank: agencyBank, bic: agencyBic }, + }) + + return new Response(html, { + headers: { + 'content-type': 'text/html; charset=utf-8', + 'content-disposition': `inline; filename="${invoice.number}.html"`, + 'cache-control': 'no-store', + }, + }) + } catch (err) { + const msg = err instanceof Error ? err.message : 'internal server error' + return NextResponse.json({ error: msg }, { status: 500 }) + } +} + +function generateInvoiceHTML({ invoice, agency }: { + invoice: { + number: string; status: string; type: string; issueDate: Date; dueDate: Date; + netCents: number; taxBp: number; grossCents: number; + zugferd: boolean; xrechnung: boolean; + company: { name: string; legalForm: string | null; city: string | null; vatId: string | null }; + lines: { description: string; quantity: number; unitCents: number; totalCents: number }[]; + }; + agency: { name: string; legalForm: string; vatId: string; registerCourt: string; registerNumber: string; city: string; bank: string; bic: string }; +}): string { + const fmtDate = (d: Date) => d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) + const fmtEuro = (cents: number) => (cents / 100).toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' €' + const taxPct = (invoice.taxBp / 100).toFixed(0) + + return ` + +
+ + +${agency.city}
+USt-IdNr.: ${agency.vatId}
+${agency.registerCourt} ${agency.registerNumber}
+${invoice.company.name}
+ ${invoice.company.legalForm ? `${invoice.company.legalForm}
` : ''} + ${invoice.company.city ? `${invoice.company.city}
` : ''} + ${invoice.company.vatId ? `USt-IdNr.: ${invoice.company.vatId}
` : ''} +| Beschreibung | +Menge | +Einzelpreis | +Gesamt | +
|---|---|---|---|
| ${l.description} | +${l.quantity} | +${fmtEuro(l.unitCents)} | +${fmtEuro(l.totalCents)} | +
| Netto | ${fmtEuro(invoice.netCents)} |
| USt (${taxPct}%) | ${fmtEuro(invoice.grossCents - invoice.netCents)} |
| Gesamt | ${fmtEuro(invoice.grossCents)} |