102a8603-26df-462f-a56d-01cee292f6f6
This commit is contained in:
1 parent
40805e16cd
commit
85c4006803
19 files changed
+987
No files matched your search
@@ -0,0 +1,48 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// POST /api/aos/accounting/bank/[id]/match — match a bank transaction to an invoice.
|
||||
// Body: { invoiceId } — marks the transaction matched + invoice paid.
|
||||
export async function POST(
|
||||
req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params
|
||||
const { invoiceId } = (await req.json()) as { invoiceId: string }
|
||||
|
||||
const tx = await db.bankTransaction.findUnique({ where: { id } })
|
||||
if (!tx) return NextResponse.json({ error: 'transaction not found' }, { status: 404 })
|
||||
|
||||
const invoice = await db.invoice.findUnique({ where: { id: invoiceId } })
|
||||
if (!invoice) return NextResponse.json({ error: 'invoice not found' }, { status: 404 })
|
||||
|
||||
const updated = await db.bankTransaction.update({
|
||||
where: { id },
|
||||
data: {
|
||||
matchedInvoiceId: invoiceId,
|
||||
matchedStatus: 'matched',
|
||||
matchReason: 'Manuell zugeordnet',
|
||||
},
|
||||
})
|
||||
|
||||
// mark invoice as paid
|
||||
await db.invoice.update({
|
||||
where: { id: invoiceId },
|
||||
data: { status: 'paid', paidDate: tx.valueDate },
|
||||
})
|
||||
|
||||
await db.auditLog.create({
|
||||
data: {
|
||||
actor: 'nora@nordlicht.digital',
|
||||
action: 'bank.match:invoice',
|
||||
entity: 'BankTransaction',
|
||||
entityId: id,
|
||||
after: JSON.stringify({ invoiceId, invoiceNumber: invoice.number, status: 'matched', invoiceStatus: 'paid' }),
|
||||
hash: 'sha256:' + Math.random().toString(36).slice(2),
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ transaction: updated, invoiceNumber: invoice.number })
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// GET /api/aos/accounting — accounting overview:
|
||||
// - VAT returns (Umsatzsteuer-Voranmeldung) with Kennzahlen
|
||||
// - EÜR (Einnahmen-Überschuss-Rechnung) from invoices + expenses
|
||||
// - Bank transactions (CAMT.053 import) with match status
|
||||
// - Dunning (Mahnwesen) for overdue invoices
|
||||
export async function GET() {
|
||||
const [vatReturns, bankTx, invoices, expenses, dunning] = await Promise.all([
|
||||
db.vatReturn.findMany({ orderBy: [{ periodYear: 'desc' }, { periodMonth: 'desc' }] }),
|
||||
db.bankTransaction.findMany({ orderBy: { bookingDate: 'desc' } }),
|
||||
db.invoice.findMany({ where: { status: { in: ['paid', 'sent', 'overdue'] } }, include: { company: true } }),
|
||||
db.expense.findMany(),
|
||||
db.dunningRun.findMany({ orderBy: { sentAt: 'desc' }, include: { invoice: { include: { company: true } } } }),
|
||||
])
|
||||
|
||||
// --- VAT stats ---
|
||||
const currentVat = vatReturns.find((v) => v.status === 'computed' || v.status === 'draft')
|
||||
const vatHistory = vatReturns.filter((v) => v.status === 'accepted')
|
||||
const totalPayableYTD = vatReturns.filter((v) => v.status === 'accepted').reduce((s, v) => s + v.payableCents, 0)
|
||||
|
||||
// --- EÜR (income statement) ---
|
||||
// Income = paid invoices (net), Cost = expenses (net, non-billable internal)
|
||||
const now = new Date()
|
||||
const eurMonths: { label: string; incomeCents: number; costCents: number; profitCents: number }[] = []
|
||||
for (let i = 5; i >= 0; i--) {
|
||||
const m = new Date(now.getFullYear(), now.getMonth() - i, 1)
|
||||
const label = m.toLocaleDateString('de-DE', { month: 'short' })
|
||||
const incomeCents = invoices
|
||||
.filter((inv) => inv.status === 'paid' && inv.paidDate && inv.paidDate.getMonth() === m.getMonth() && inv.paidDate.getFullYear() === m.getFullYear())
|
||||
.reduce((s, i) => s + i.netCents, 0)
|
||||
const costCents = expenses
|
||||
.filter((e) => e.date.getMonth() === m.getMonth() && e.date.getFullYear() === m.getFullYear())
|
||||
.reduce((s, e) => s + e.amountCents, 0)
|
||||
eurMonths.push({ label, incomeCents, costCents, profitCents: incomeCents - costCents })
|
||||
}
|
||||
const totalIncome = eurMonths.reduce((s, m) => s + m.incomeCents, 0)
|
||||
const totalCost = eurMonths.reduce((s, m) => s + m.costCents, 0)
|
||||
const totalProfit = totalIncome - totalCost
|
||||
|
||||
// EÜR by category (costs)
|
||||
const costsByCategory: Record<string, number> = {}
|
||||
for (const e of expenses) {
|
||||
costsByCategory[e.categoryId] = (costsByCategory[e.categoryId] ?? 0) + e.amountCents
|
||||
}
|
||||
|
||||
// --- Bank reconciliation ---
|
||||
const matched = bankTx.filter((t) => t.matchedStatus === 'matched').length
|
||||
const suggested = bankTx.filter((t) => t.matchedStatus === 'suggested').length
|
||||
const unmatched = bankTx.filter((t) => t.matchedStatus === 'unmatched').length
|
||||
const inflowCents = bankTx.filter((t) => t.amountCents > 0).reduce((s, t) => s + t.amountCents, 0)
|
||||
const outflowCents = bankTx.filter((t) => t.amountCents < 0).reduce((s, t) => s + t.amountCents, 0)
|
||||
|
||||
// --- Dunning ---
|
||||
const overdueInvoices = invoices.filter((i) => i.status === 'overdue')
|
||||
|
||||
return NextResponse.json({
|
||||
vat: {
|
||||
current: currentVat,
|
||||
history: vatHistory,
|
||||
totalPayableYTD,
|
||||
stats: { periods: vatReturns.length, accepted: vatHistory.length, pending: currentVat ? 1 : 0 },
|
||||
},
|
||||
eur: {
|
||||
months: eurMonths,
|
||||
totalIncome,
|
||||
totalCost,
|
||||
totalProfit,
|
||||
costsByCategory: Object.entries(costsByCategory).map(([cat, cents]) => ({ category: cat, cents })).sort((a, b) => b.cents - a.cents),
|
||||
},
|
||||
bank: {
|
||||
transactions: bankTx,
|
||||
stats: { matched, suggested, unmatched, inflowCents, outflowCents, total: bankTx.length },
|
||||
},
|
||||
dunning: {
|
||||
overdue: overdueInvoices.map((i) => ({ id: i.id, number: i.number, companyName: i.company.name, grossCents: i.grossCents, dueDate: i.dueDate, daysOverdue: Math.round((Date.now() - i.dueDate.getTime()) / 86400000) })),
|
||||
runs: dunning,
|
||||
stats: { overdueCount: overdueInvoices.length, overdueCents: overdueInvoices.reduce((s, i) => s + i.grossCents, 0), dunningSent: dunning.length },
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// PATCH /api/aos/accounting/vat/[id]/submit — submit VAT return to ELSTER (mock ERiC).
|
||||
// Transitions computed → submitted → accepted, generates elsterRef, writes audit.
|
||||
export async function PATCH(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params
|
||||
const vat = await db.vatReturn.findUnique({ where: { id } })
|
||||
if (!vat) return NextResponse.json({ error: 'not found' }, { status: 404 })
|
||||
if (vat.status === 'accepted') return NextResponse.json({ error: 'already accepted' }, { status: 409 })
|
||||
|
||||
const monthName = new Date(vat.periodYear, vat.periodMonth - 1, 1).toLocaleDateString('de-DE', { month: 'short' })
|
||||
const elsterRef = `ERiC-${vat.periodYear}-${String(vat.periodMonth).padStart(2, '0')}-NORDLICHT-${Math.floor(Math.random() * 9000 + 1000)}`
|
||||
|
||||
const updated = await db.vatReturn.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'accepted',
|
||||
submittedAt: new Date(),
|
||||
acceptedAt: new Date(),
|
||||
elsterRef,
|
||||
notes: `Umsatzsteuer-Voranmeldung ${monthName} ${vat.periodYear} via ERiC übermittelt und vom Finanzamt bestätigt.`,
|
||||
},
|
||||
})
|
||||
|
||||
await db.auditLog.create({
|
||||
data: {
|
||||
actor: 'nora@nordlicht.digital',
|
||||
action: 'vat.submit:elster',
|
||||
entity: 'VatReturn',
|
||||
entityId: id,
|
||||
before: JSON.stringify({ status: vat.status }),
|
||||
after: JSON.stringify({ status: 'accepted', elsterRef, payableCents: vat.payableCents }),
|
||||
hash: 'sha256:' + Math.random().toString(36).slice(2),
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ vat: updated, elsterRef })
|
||||
}
|
||||
@@ -15,6 +15,9 @@ export async function POST() {
|
||||
try {
|
||||
// wipe via prisma (SQLite TRUNCATE not supported, use deleteMany per model in order)
|
||||
const { db } = await import('@/lib/db')
|
||||
await db.dunningRun.deleteMany()
|
||||
await db.vatReturn.deleteMany()
|
||||
await db.bankTransaction.deleteMany()
|
||||
await db.estimateLine.deleteMany()
|
||||
await db.estimate.deleteMany()
|
||||
await db.ticketMessage.deleteMany()
|
||||
|
||||
@@ -18,6 +18,7 @@ import { LicensesView } from './views/licenses'
|
||||
import { ContractsView } from './views/contracts'
|
||||
import { ExpensesView } from './views/expenses'
|
||||
import { FinanceView } from './views/finance'
|
||||
import { AccountingView } from './views/accounting'
|
||||
import { AuditView } from './views/audit'
|
||||
import { SettingsView } from './views/settings'
|
||||
import { SetupWizardView } from './views/setup-wizard'
|
||||
@@ -57,6 +58,7 @@ export function AppShell() {
|
||||
{view === 'contracts' && <ContractsView />}
|
||||
{view === 'expenses' && <ExpensesView />}
|
||||
{view === 'finance' && <FinanceView />}
|
||||
{view === 'accounting' && <AccountingView />}
|
||||
{view === 'audit' && <AuditView />}
|
||||
{view === 'settings' && <SettingsView />}
|
||||
{view === 'setup' && <SetupWizardView />}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
LifeBuoy,
|
||||
BarChart3,
|
||||
Rocket,
|
||||
BookOpen,
|
||||
} from 'lucide-react'
|
||||
import { useAos, type ViewId } from '@/store/aos'
|
||||
import {
|
||||
@@ -45,6 +46,7 @@ const VIEWS: { id: ViewId; label: string; icon: typeof LayoutDashboard; hint: st
|
||||
{ id: 'contracts', label: 'Verträge', icon: FileSignature, hint: 'Werkvertrag, AVV, NDA' },
|
||||
{ id: 'expenses', label: 'Ausgaben', icon: Wallet, hint: 'Belastbar & DATEV' },
|
||||
{ id: 'finance', label: 'Finanzen', icon: Receipt, hint: 'Rechnungen & E-Rechnung' },
|
||||
{ id: 'accounting', label: 'Buchhaltung', icon: BookOpen, hint: 'USt-Voranmeldung, EÜR, Bank' },
|
||||
{ id: 'audit', label: 'Audit-Protokoll', icon: ScrollText, hint: 'Hash-Kette & GoBD' },
|
||||
{ id: 'settings', label: 'Erweiterungen', icon: Settings2, hint: 'Module & Compliance' },
|
||||
{ id: 'setup', label: 'Setup-Wizard', icon: Rocket, hint: '≤ 15 Min Installation' },
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
LifeBuoy,
|
||||
BarChart3,
|
||||
Rocket,
|
||||
BookOpen,
|
||||
} from 'lucide-react'
|
||||
import { useAos, type ViewId } from '@/store/aos'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -57,6 +58,7 @@ const NAV: {
|
||||
{ id: 'contracts', label: 'Verträge', icon: FileSignature, permission: 'contracts:read' },
|
||||
{ id: 'expenses', label: 'Ausgaben', icon: Wallet, permission: 'expenses:read' },
|
||||
{ id: 'finance', label: 'Finanzen', icon: Receipt, permission: 'finance:read' },
|
||||
{ id: 'accounting', label: 'Buchhaltung', icon: BookOpen, permission: 'accounting:read' },
|
||||
{ id: 'audit', label: 'Audit-Protokoll', icon: ScrollText, permission: '*' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -27,6 +27,7 @@ const TITLES: Record<ViewId, { title: string; crumb: string }> = {
|
||||
contracts: { title: 'Verträge', crumb: 'Betrieb / Verträge' },
|
||||
expenses: { title: 'Ausgaben', crumb: 'Betrieb / Ausgaben' },
|
||||
finance: { title: 'Finanzen', crumb: 'Betrieb / Finanzen' },
|
||||
accounting: { title: 'Buchhaltung', crumb: 'Betrieb / Buchhaltung' },
|
||||
audit: { title: 'Audit-Protokoll', crumb: 'Betrieb / Audit' },
|
||||
settings: { title: 'Erweiterungen', crumb: 'System / Module' },
|
||||
setup: { title: 'Setup-Wizard', crumb: 'System / Setup' },
|
||||
|
||||
@@ -0,0 +1,585 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
BookOpen,
|
||||
Landmark,
|
||||
TrendingUp,
|
||||
ArrowDownLeft,
|
||||
ArrowUpRight,
|
||||
Send,
|
||||
CheckCircle2,
|
||||
AlertTriangle,
|
||||
Loader2,
|
||||
Link2,
|
||||
Banknote,
|
||||
} from 'lucide-react'
|
||||
import { useFetch } from '@/hooks/use-fetch'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
|
||||
import { PageHeader } from '../primitives'
|
||||
import { formatEuro, formatEuroCompact, formatDate, formatDateTime } from '@/lib/format'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { toast } from 'sonner'
|
||||
import { useState } from 'react'
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis, Cell, Line, LineChart, CartesianGrid } from 'recharts'
|
||||
|
||||
interface VatReturn {
|
||||
id: string
|
||||
periodYear: number
|
||||
periodMonth: number
|
||||
periodType: string
|
||||
status: string
|
||||
k81StandardNetCents: number
|
||||
k86ReducedNetCents: number
|
||||
k66TaxFreeCents: number
|
||||
k83TaxStandardCents: number
|
||||
k65TaxReducedCents: number
|
||||
k37InputTaxCents: number
|
||||
outputTaxCents: number
|
||||
inputTaxCents: number
|
||||
payableCents: number
|
||||
submittedAt: string | null
|
||||
elsterRef: string | null
|
||||
acceptedAt: string | null
|
||||
notes: string | null
|
||||
}
|
||||
interface BankTransaction {
|
||||
id: string
|
||||
amountCents: number
|
||||
bookingDate: string
|
||||
valueDate: string
|
||||
partnerName: string | null
|
||||
partnerIban: string | null
|
||||
reference: string | null
|
||||
purpose: string | null
|
||||
matchedStatus: string
|
||||
matchReason: string | null
|
||||
}
|
||||
interface AccountingData {
|
||||
vat: {
|
||||
current: VatReturn | null
|
||||
history: VatReturn[]
|
||||
totalPayableYTD: number
|
||||
stats: { periods: number; accepted: number; pending: number }
|
||||
}
|
||||
eur: {
|
||||
months: { label: string; incomeCents: number; costCents: number; profitCents: number }[]
|
||||
totalIncome: number
|
||||
totalCost: number
|
||||
totalProfit: number
|
||||
costsByCategory: { category: string; cents: number }[]
|
||||
}
|
||||
bank: {
|
||||
transactions: BankTransaction[]
|
||||
stats: { matched: number; suggested: number; unmatched: number; inflowCents: number; outflowCents: number; total: number }
|
||||
}
|
||||
dunning: {
|
||||
overdue: { id: string; number: string; companyName: string; grossCents: number; dueDate: string; daysOverdue: number }[]
|
||||
runs: { id: string; level: number; status: string; feesCents: number; sentAt: string; invoice: { number: string; company: { name: string } } }[]
|
||||
stats: { overdueCount: number; overdueCents: number; dunningSent: number }
|
||||
}
|
||||
}
|
||||
|
||||
const VAT_STATUS_META: Record<string, { label: string; cls: string }> = {
|
||||
draft: { label: 'Entwurf', cls: 'bg-muted text-foreground' },
|
||||
computed: { label: 'Berechnet', cls: 'bg-foreground/50 text-background' },
|
||||
submitted: { label: 'Übermittelt', cls: 'bg-foreground/70 text-background' },
|
||||
accepted: { label: 'Akzeptiert', cls: 'bg-foreground text-background' },
|
||||
corrected: { label: 'Korrigiert', cls: 'bg-muted text-muted-foreground' },
|
||||
}
|
||||
|
||||
const MONTH_NAMES = ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']
|
||||
|
||||
const CATEGORY_LABELS: Record<string, string> = {
|
||||
reise: 'Reise', software: 'Software', hardware: 'Hardware', fremdleistung: 'Fremdleistung', buero: 'Büro', marketing: 'Marketing',
|
||||
}
|
||||
|
||||
export function AccountingView() {
|
||||
const { data, loading, refetch } = useFetch<AccountingData>('/api/aos/accounting')
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<div className="space-y-6 p-4 md:p-6">
|
||||
<Skeleton className="h-9 w-40" />
|
||||
<Skeleton className="h-40" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const { vat, eur, bank, dunning } = data
|
||||
|
||||
const submitVat = async (vatId: string) => {
|
||||
setSubmitting(true)
|
||||
const t = toast.loading('Voranmeldung wird an ELSTER übermittelt (ERiC)…')
|
||||
try {
|
||||
const r = await fetch(`/api/aos/accounting/vat/${vatId}/submit`, { method: 'PATCH' })
|
||||
if (!r.ok) {
|
||||
const err = await r.json().catch(() => ({}))
|
||||
throw new Error(err.error || 'submit failed')
|
||||
}
|
||||
const res = await r.json()
|
||||
toast.success(`Akzeptiert — ELSTER-Ref: ${res.elsterRef}`, { id: t })
|
||||
refetch()
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Übermittlung fehlgeschlagen', { id: t })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const matchTx = async (txId: string, invoiceId: string) => {
|
||||
const t = toast.loading('Bankbewegung wird zugeordnet…')
|
||||
try {
|
||||
const r = await fetch(`/api/aos/accounting/bank/${txId}/match`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ invoiceId }),
|
||||
})
|
||||
if (!r.ok) throw new Error('match failed')
|
||||
toast.success('Zugeordnet — Rechnung als bezahlt markiert', { id: t })
|
||||
refetch()
|
||||
} catch {
|
||||
toast.error('Zuordnung fehlgeschlagen', { id: t })
|
||||
}
|
||||
}
|
||||
|
||||
const currentVat = vat.current
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-4 md:p-6">
|
||||
<PageHeader
|
||||
title="Buchhaltung"
|
||||
description="Alman muhasebe uyumlu: Umsatzsteuer-Voranmeldung (ELSTER), EÜR, Bankabgleich (CAMT.053) und Mahnwesen. GoBD-konform mit Audit-Hash-Kette."
|
||||
/>
|
||||
|
||||
{/* KPIs */}
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-muted-foreground">USt-Zahllast (lfd. Monat)</p>
|
||||
<Landmark className="size-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="mt-1 text-2xl font-semibold tabular-nums">{currentVat ? formatEuroCompact(currentVat.payableCents) : '—'}</p>
|
||||
<p className="text-[10px] text-muted-foreground">{currentVat ? `${MONTH_NAMES[currentVat.periodMonth - 1]} ${currentVat.periodYear}` : 'keine offene Periode'}</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-muted-foreground">USt YTD (abgeführt)</p>
|
||||
<CheckCircle2 className="size-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="mt-1 text-2xl font-semibold tabular-nums">{formatEuroCompact(vat.totalPayableYTD)}</p>
|
||||
<p className="text-[10px] text-muted-foreground">{vat.stats.accepted} Perioden akzeptiert</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-muted-foreground">EÜR Gewinn (6 Mo)</p>
|
||||
<TrendingUp className="size-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="mt-1 text-2xl font-semibold tabular-nums">{formatEuroCompact(eur.totalProfit)}</p>
|
||||
<p className="text-[10px] text-muted-foreground">{formatEuroCompact(eur.totalIncome)} Einnahmen</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-muted-foreground">Überfällige Posten</p>
|
||||
<AlertTriangle className="size-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="mt-1 text-2xl font-semibold tabular-nums">{formatEuroCompact(dunning.stats.overdueCents)}</p>
|
||||
<p className="text-[10px] text-muted-foreground">{dunning.stats.overdueCount} Rechnungen</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="vat">
|
||||
<TabsList>
|
||||
<TabsTrigger value="vat">Umsatzsteuer</TabsTrigger>
|
||||
<TabsTrigger value="eur">EÜR</TabsTrigger>
|
||||
<TabsTrigger value="bank">Bankabgleich</TabsTrigger>
|
||||
<TabsTrigger value="dunning">Mahnwesen</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* VAT tab */}
|
||||
<TabsContent value="vat" className="mt-4 space-y-4">
|
||||
{currentVat && (
|
||||
<Card>
|
||||
<CardHeader className="border-b border-border pb-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<CardTitle className="text-base">
|
||||
Voranmeldung {MONTH_NAMES[currentVat.periodMonth - 1]} {currentVat.periodYear}
|
||||
</CardTitle>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{currentVat.periodType === 'monthly' ? 'Monatlich' : 'Quartalsweise'} ·{' '}
|
||||
<Badge variant="outline" className={cn('text-[10px]', (VAT_STATUS_META[currentVat.status] ?? VAT_STATUS_META.draft).cls)}>
|
||||
{(VAT_STATUS_META[currentVat.status] ?? VAT_STATUS_META.draft).label}
|
||||
</Badge>
|
||||
</p>
|
||||
</div>
|
||||
{currentVat.status === 'computed' && (
|
||||
<Button size="sm" className="gap-1.5" onClick={() => submitVat(currentVat.id)} disabled={submitting}>
|
||||
{submitting ? <Loader2 className="size-3.5 animate-spin" /> : <Send className="size-3.5" />}
|
||||
An ELSTER übermitteln
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4">
|
||||
{/* Kennzahlen */}
|
||||
<div className="mb-4">
|
||||
<p className="mb-2 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">Kennzahlen (UStVA)</p>
|
||||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-6">
|
||||
<Kennzahl kz="81" label="19% Umsatz (netto)" value={currentVat.k81StandardNetCents} />
|
||||
<Kennzahl kz="86" label="7% Umsatz (netto)" value={currentVat.k86ReducedNetCents} />
|
||||
<Kennzahl kz="66" label="Steuerfrei §4" value={currentVat.k66TaxFreeCents} />
|
||||
<Kennzahl kz="83" label="Steuer 19%" value={currentVat.k83TaxStandardCents} />
|
||||
<Kennzahl kz="65" label="Steuer 7%" value={currentVat.k65TaxReducedCents} />
|
||||
<Kennzahl kz="37" label="Vorsteuer" value={currentVat.k37InputTaxCents} />
|
||||
</div>
|
||||
</div>
|
||||
{/* Calculation */}
|
||||
<div className="space-y-1.5 rounded-md border border-border bg-muted/20 p-3 text-xs">
|
||||
<CalcRow label="Umsatzsteuer (Summe 83+65)" value={currentVat.outputTaxCents} />
|
||||
<CalcRow label="Abziehbare Vorsteuer (37)" value={currentVat.inputTaxCents} negative />
|
||||
<div className="mt-1 flex items-center justify-between border-t border-border pt-1.5 font-semibold">
|
||||
<span>Zahllast / Gutschrift</span>
|
||||
<span className={cn('tabular-nums', currentVat.payableCents < 0 && 'text-muted-foreground')}>
|
||||
{currentVat.payableCents < 0 ? '−' : ''}{formatEuro(Math.abs(currentVat.payableCents))}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{currentVat.notes && (
|
||||
<p className="mt-3 text-[11px] text-muted-foreground">{currentVat.notes}</p>
|
||||
)}
|
||||
<div className="mt-3 rounded-md border border-dashed border-border p-3 text-[11px] text-muted-foreground">
|
||||
<p className="font-medium text-foreground">ELSTER-Übermittlung (ERiC)</p>
|
||||
<p className="mt-1">Die Kennzahlen werden aus den gebuchten Rechnungen und Ausgaben berechnet (Ist-Versteuerung). Übermittlung via ERiC-Schnittstelle. Vor dem Absenden: Vorprüfung jeder Kennzahl mit Herkunftsnachweis.</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* VAT history */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm">Historie ({vat.history.length} Perioden)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="border-y border-border bg-muted/40 text-left text-[11px] uppercase tracking-wide text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-4 py-2 font-medium">Periode</th>
|
||||
<th className="px-4 py-2 font-medium text-right">81 (19% netto)</th>
|
||||
<th className="px-4 py-2 font-medium text-right">37 (Vorsteuer)</th>
|
||||
<th className="px-4 py-2 font-medium text-right">Zahllast</th>
|
||||
<th className="px-4 py-2 font-medium">ELSTER-Ref</th>
|
||||
<th className="px-4 py-2 font-medium">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{vat.history.map((v) => (
|
||||
<tr key={v.id} className="transition hover:bg-muted/40">
|
||||
<td className="px-4 py-2.5 text-xs font-medium">{MONTH_NAMES[v.periodMonth - 1]} {v.periodYear}</td>
|
||||
<td className="px-4 py-2.5 text-right text-xs tabular-nums">{formatEuro(v.k81StandardNetCents)}</td>
|
||||
<td className="px-4 py-2.5 text-right text-xs tabular-nums">{formatEuro(v.k37InputTaxCents)}</td>
|
||||
<td className="px-4 py-2.5 text-right text-xs font-semibold tabular-nums">{formatEuro(v.payableCents)}</td>
|
||||
<td className="px-4 py-2.5"><code className="text-[10px] font-mono text-muted-foreground">{v.elsterRef ?? '—'}</code></td>
|
||||
<td className="px-4 py-2.5">
|
||||
<Badge variant="outline" className={cn('text-[10px]', (VAT_STATUS_META[v.status] ?? VAT_STATUS_META.draft).cls)}>
|
||||
{(VAT_STATUS_META[v.status] ?? VAT_STATUS_META.draft).label}
|
||||
</Badge>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* EÜR tab */}
|
||||
<TabsContent value="eur" className="mt-4 space-y-4">
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||||
<Card className="lg:col-span-2">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm">Einnahmen vs. Ausgaben (6 Monate, netto)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<BarChart data={eur.months} margin={{ top: 4, right: 4, bottom: 0, left: -20 }}>
|
||||
<XAxis dataKey="label" tick={{ fontSize: 11, fill: 'var(--muted-foreground)' }} axisLine={false} tickLine={false} />
|
||||
<YAxis tick={{ fontSize: 11, fill: 'var(--muted-foreground)' }} axisLine={false} tickLine={false} tickFormatter={(v) => formatEuroCompact(v).replace(' €', '')} width={50} />
|
||||
<Tooltip
|
||||
cursor={{ fill: 'var(--muted)', opacity: 0.4 }}
|
||||
contentStyle={{ background: 'var(--popover)', border: '1px solid var(--border)', borderRadius: 'var(--radius)', fontSize: 12 }}
|
||||
formatter={(v: number, name: string) => [formatEuro(v), name === 'incomeCents' ? 'Einnahmen' : 'Ausgaben']}
|
||||
/>
|
||||
<Bar dataKey="incomeCents" fill="var(--foreground)" radius={[3, 3, 0, 0]} />
|
||||
<Bar dataKey="costCents" fill="var(--muted-foreground)" fillOpacity={0.5} radius={[3, 3, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm">Gewinnverlauf</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<LineChart data={eur.months} margin={{ top: 4, right: 4, bottom: 0, left: -20 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" vertical={false} />
|
||||
<XAxis dataKey="label" tick={{ fontSize: 11, fill: 'var(--muted-foreground)' }} axisLine={false} tickLine={false} />
|
||||
<YAxis tick={{ fontSize: 11, fill: 'var(--muted-foreground)' }} axisLine={false} tickLine={false} tickFormatter={(v) => formatEuroCompact(v).replace(' €', '')} width={50} />
|
||||
<Tooltip
|
||||
contentStyle={{ background: 'var(--popover)', border: '1px solid var(--border)', borderRadius: 'var(--radius)', fontSize: 12 }}
|
||||
formatter={(v: number) => [formatEuro(v), 'Gewinn']}
|
||||
/>
|
||||
<Line type="monotone" dataKey="profitCents" stroke="var(--foreground)" strokeWidth={2} dot={{ fill: 'var(--foreground)', r: 3 }} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm">EÜR-Summe (6 Monate)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1.5 text-xs">
|
||||
<CalcRow label="Einnahmen (bezahlte Rechnungen, netto)" value={eur.totalIncome} />
|
||||
<CalcRow label="Ausgaben (Aufwendungen, netto)" value={eur.totalCost} negative />
|
||||
<div className="mt-1 flex items-center justify-between border-t border-border pt-1.5 font-semibold">
|
||||
<span>Gewinn (Einnahmen-Überschuss)</span>
|
||||
<span className="tabular-nums">{formatEuro(eur.totalProfit)}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm">Ausgaben nach Kategorie</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{eur.costsByCategory.map((c) => {
|
||||
const max = eur.costsByCategory[0]?.cents ?? 1
|
||||
const pct = Math.round((c.cents / max) * 100)
|
||||
return (
|
||||
<div key={c.category}>
|
||||
<div className="mb-1 flex items-center justify-between text-xs">
|
||||
<span className="font-medium">{CATEGORY_LABELS[c.category] ?? c.category}</span>
|
||||
<span className="tabular-nums text-muted-foreground">{formatEuro(c.cents)}</span>
|
||||
</div>
|
||||
<div className="h-1.5 w-full overflow-hidden rounded bg-muted">
|
||||
<div className="h-full rounded bg-foreground" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border border-dashed border-border p-3 text-[11px] text-muted-foreground">
|
||||
<p className="font-medium text-foreground">EÜR (Einnahmen-Überschuss-Rechnung)</p>
|
||||
<p className="mt-1">Für Freiberufler und Kleingewerbe (§ 4 Abs. 3 EStG). Einnahmen bei Ist-Versteuerung nach Zufluss, Ausgaben nach Abfluss. Jahres-EÜR mit AVEÜR an das Finanzamt (Anlage EÜR). AfA und pauschalen werden separat berechnet.</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Bank reconciliation tab */}
|
||||
<TabsContent value="bank" className="mt-4 space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-muted-foreground">Zugeordnet</p>
|
||||
<CheckCircle2 className="size-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="mt-1 text-xl font-semibold tabular-nums">{bank.stats.matched}</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-muted-foreground">Vorgeschlagen</p>
|
||||
<Link2 className="size-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="mt-1 text-xl font-semibold tabular-nums">{bank.stats.suggested}</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-muted-foreground">Zugänge</p>
|
||||
<ArrowDownLeft className="size-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="mt-1 text-xl font-semibold tabular-nums">{formatEuroCompact(bank.stats.inflowCents)}</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-muted-foreground">Abgänge</p>
|
||||
<ArrowUpRight className="size-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="mt-1 text-xl font-semibold tabular-nums">{formatEuroCompact(bank.stats.outflowCents)}</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm">Bankbewegungen (CAMT.053 Import)</CardTitle>
|
||||
<Button size="sm" variant="outline" className="h-7 gap-1.5 text-xs">
|
||||
<Banknote className="size-3.5" />
|
||||
CAMT.053 importieren
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="border-y border-border bg-muted/40 text-left text-[11px] uppercase tracking-wide text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-4 py-2 font-medium">Buchungstag</th>
|
||||
<th className="px-4 py-2 font-medium">Partner</th>
|
||||
<th className="px-4 py-2 font-medium">Verwendungszweck</th>
|
||||
<th className="px-4 py-2 font-medium text-right">Betrag</th>
|
||||
<th className="px-4 py-2 font-medium">Status</th>
|
||||
<th className="px-4 py-2 font-medium">Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{bank.transactions.map((tx) => {
|
||||
const isIncome = tx.amountCents > 0
|
||||
return (
|
||||
<tr key={tx.id} className="transition hover:bg-muted/40">
|
||||
<td className="px-4 py-2.5 text-xs">{formatDate(tx.bookingDate)}</td>
|
||||
<td className="px-4 py-2.5 text-xs">
|
||||
<p className="font-medium">{tx.partnerName ?? '—'}</p>
|
||||
{tx.partnerIban && <p className="text-[10px] font-mono text-muted-foreground">{tx.partnerIban}</p>}
|
||||
</td>
|
||||
<td className="px-4 py-2.5">
|
||||
<p className="truncate text-xs">{tx.purpose ?? tx.reference ?? '—'}</p>
|
||||
{tx.matchReason && <p className="text-[10px] text-muted-foreground">{tx.matchReason}</p>}
|
||||
</td>
|
||||
<td className={cn('px-4 py-2.5 text-right text-xs font-semibold tabular-nums', isIncome ? 'text-foreground' : 'text-muted-foreground')}>
|
||||
{isIncome ? '+' : ''}{formatEuro(tx.amountCents)}
|
||||
</td>
|
||||
<td className="px-4 py-2.5">
|
||||
<Badge variant="outline" className={cn(
|
||||
'text-[10px]',
|
||||
tx.matchedStatus === 'matched' && 'border-foreground/40',
|
||||
tx.matchedStatus === 'suggested' && 'border-foreground/30',
|
||||
)}>
|
||||
{tx.matchedStatus === 'matched' ? 'Zugeordnet' : tx.matchedStatus === 'suggested' ? 'Vorschlag' : 'Offen'}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-2.5">
|
||||
{tx.matchedStatus === 'suggested' && (
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-7 gap-1 text-[11px]"
|
||||
onClick={() => matchTx(tx.id, 'demo')}
|
||||
>
|
||||
<CheckCircle2 className="size-3" />Zuordnen
|
||||
</Button>
|
||||
)}
|
||||
{tx.matchedStatus === 'unmatched' && (
|
||||
<span className="text-[10px] text-muted-foreground">—</span>
|
||||
)}
|
||||
{tx.matchedStatus === 'matched' && (
|
||||
<CheckCircle2 className="size-4 text-foreground" />
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Dunning tab */}
|
||||
<TabsContent value="dunning" className="mt-4 space-y-4">
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-sm">
|
||||
<AlertTriangle className="size-3.5 text-muted-foreground" />
|
||||
Überfällige Rechnungen ({dunning.overdue.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{dunning.overdue.length === 0 && (
|
||||
<p className="py-6 text-center text-xs text-muted-foreground">Keine überfälligen Rechnungen.</p>
|
||||
)}
|
||||
{dunning.overdue.map((inv) => (
|
||||
<div key={inv.id} className="rounded-md border border-border p-2.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<code className="text-[11px] font-mono">{inv.number}</code>
|
||||
<Badge variant="outline" className="text-[10px]">{inv.daysOverdue} d überfällig</Badge>
|
||||
</div>
|
||||
<p className="mt-1 truncate text-xs">{inv.companyName}</p>
|
||||
<div className="mt-1 flex items-center justify-between">
|
||||
<span className="text-[10px] text-muted-foreground">Fällig: {formatDate(inv.dueDate)}</span>
|
||||
<span className="text-xs font-semibold tabular-nums">{formatEuro(inv.grossCents)}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm">Mahnlauf-Historie ({dunning.runs.length})</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{dunning.runs.length === 0 && (
|
||||
<p className="py-6 text-center text-xs text-muted-foreground">Keine Mahnungen versendet.</p>
|
||||
)}
|
||||
{dunning.runs.map((run) => (
|
||||
<div key={run.id} className="rounded-md border border-border p-2.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge variant="outline" className="text-[10px]">Mahnstufe {run.level}</Badge>
|
||||
<span className="text-[10px] text-muted-foreground">{formatDateTime(run.sentAt)}</span>
|
||||
</div>
|
||||
<p className="mt-1 truncate text-xs">{run.invoice.company.name} · <code className="font-mono text-[10px]">{run.invoice.number}</code></p>
|
||||
{run.feesCents > 0 && (
|
||||
<p className="mt-1 text-[10px] text-muted-foreground">Mahngebühren: {formatEuro(run.feesCents)}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border border-dashed border-border p-3 text-[11px] text-muted-foreground">
|
||||
<p className="font-medium text-foreground">Mahnwesen (3 Stufen)</p>
|
||||
<p className="mt-1">Stufe 1: Zahlungserinnerung (kostenlos) · Stufe 2: Mahnung mit Gebühr (2,50 €) · Stufe 3: Letzte Mahnung + Verzugszinsen (§ 288 BGB, 5 % p.a.). Nach Stufe 3: automatische Suspendierung verknüpfter Lizenzen (konfigurierbar).</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Kennzahl({ kz, label, value }: { kz: string; label: string; value: number }) {
|
||||
return (
|
||||
<div className="rounded-md border border-border p-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<code className="text-[10px] font-mono font-bold">{kz}</code>
|
||||
<span className="text-[9px] text-muted-foreground">{label}</span>
|
||||
</div>
|
||||
<p className="mt-1 text-sm font-semibold tabular-nums">{formatEuro(value)}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CalcRow({ label, value, negative }: { label: string; value: number; negative?: boolean }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">{label}</span>
|
||||
<span className="tabular-nums">{negative ? '−' : ''}{formatEuro(value)}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -42,6 +42,7 @@ const MODULE_META: Record<string, { label: string; desc: string; phase: string }
|
||||
expenses: { label: 'Ausgaben', desc: 'Belastbare Posten, Wiederkehrendes, DATEV-Export.', phase: 'Phase 5' },
|
||||
estimates: { label: 'Angebote', desc: 'Quotes mit Positionen, Akzeptanz → Projekt, → Rechnung.', phase: 'Phase 5' },
|
||||
support: { label: 'Support', desc: 'Ticket-Inbox, SLA-Timer, AI-Summary & Antwort-Vorschläge.', phase: 'Phase 6' },
|
||||
accounting: { label: 'Buchhaltung', desc: 'USt-Voranmeldung (ELSTER), EÜR, Bankabgleich, Mahnwesen.', phase: 'Phase 6' },
|
||||
'sales-docs': { label: 'Angebote & Verträge', desc: 'Angebote, Auftragsbestätigung, Vertragstemplates.', phase: 'Phase 5' },
|
||||
finance: { label: 'Finanzen', desc: 'Rechnungen, E-Rechnung, Mahnwesen, DATEV.', phase: 'Phase 5' },
|
||||
reporting: { label: 'Reporting', desc: 'Dashboards: Pipeline, Auslastung, MRR (geplant).', phase: 'Later' },
|
||||
|
||||
@@ -624,6 +624,49 @@ async function main() {
|
||||
await db.auditLog.create({ data: { actor: 'sven@nordlicht.digital', action: 'estimate.send', entity: 'Estimate', entityId: 'seed', after: '{"status":"sent"}', hash: 'sha256:' + Math.random().toString(36).slice(2) } })
|
||||
await db.auditLog.create({ data: { actor: 'sven@nordlicht.digital', action: 'estimate.accept', entity: 'Estimate', entityId: 'seed', after: '{"status":"accepted","projectId":"created"}', hash: 'sha256:' + Math.random().toString(36).slice(2) } })
|
||||
|
||||
// --- Accounting: Bank transactions (CAMT.053 style) ---------------------
|
||||
const bankTx = [
|
||||
{ amountCents: CENTS(5400), bookingDate: daysFromNow(-1), valueDate: daysFromNow(-1), partnerName: 'Nordwind Logistik GmbH', partnerIban: 'DE89 3704 0044 0532 0130 00', reference: 'RE-2026-0039', purpose: 'SEPA-Überweisung RE-2026-0039', matchedStatus: 'suggested', matchReason: 'Referenz matched RE-2026-0039, Betrag exakt' },
|
||||
{ amountCents: CENTS(420), bookingDate: daysFromNow(-3), valueDate: daysFromNow(-3), partnerName: 'HanseMöbel Manufaktur GmbH', partnerIban: 'DE12 5001 0517 0000 0000 00', reference: 'RE-2026-0040', purpose: 'Wartungspauschale Oktober', matchedStatus: 'suggested', matchReason: 'Referenz matched, Betrag +/- 0,02€ Toleranz' },
|
||||
{ amountCents: -CENTS(89), bookingDate: daysFromNow(-2), valueDate: daysFromNow(-2), partnerName: 'Cloudflare Inc.', partnerIban: null, reference: 'CC-2026-5512', purpose: 'Cloudflare Pro Abonnement', matchedStatus: 'unmatched' },
|
||||
{ amountCents: -CENTS(142), bookingDate: daysFromNow(-4), valueDate: daysFromNow(-4), partnerName: 'Deutsche Bahn AG', partnerIban: null, reference: 'DB-2026-8841', purpose: 'Hamburg → Berlin ICE', matchedStatus: 'unmatched' },
|
||||
{ amountCents: CENTS(8800), bookingDate: daysFromNow(-12), valueDate: daysFromNow(-11), partnerName: 'TechStart UG', partnerIban: 'DE45 1004 0000 0123 4567 89', reference: 'RE-2026-0036', purpose: 'Rechnung RE-2026-0036 SaaS MVP', matchedStatus: 'matched', matchReason: 'Manuell zugeordnet' },
|
||||
{ amountCents: -CENTS(44), bookingDate: daysFromNow(-25), valueDate: daysFromNow(-25), partnerName: 'GitHub Inc.', partnerIban: null, reference: 'GH-TEAM-2026', purpose: 'GitHub Team 10 Seats', matchedStatus: 'unmatched' },
|
||||
{ amountCents: -CENTS(899), bookingDate: daysFromNow(-8), valueDate: daysFromNow(-8), partnerName: 'JetBrains s.r.o.', partnerIban: null, reference: 'JB-2026-APPPACK', purpose: 'All Products Pack 5 Seats', matchedStatus: 'unmatched' },
|
||||
{ amountCents: CENTS(1500), bookingDate: daysFromNow(-30), valueDate: daysFromNow(-29), partnerName: 'Nordwind Logistik GmbH', partnerIban: 'DE89 3704 0044 0532 0130 00', reference: 'RE-2026-0035', purpose: 'Abschlagszahlung Projekt', matchedStatus: 'matched', matchReason: 'Manuell zugeordnet' },
|
||||
{ amountCents: -CENTS(180), bookingDate: daysFromNow(-3), valueDate: daysFromNow(-3), partnerName: 'Sectigo', partnerIban: null, reference: 'SC-WILDCARD-2026', purpose: 'SSL Wildcard-Zertifikat', matchedStatus: 'unmatched' },
|
||||
{ amountCents: -CENTS(67), bookingDate: daysFromNow(-18), valueDate: daysFromNow(-18), partnerName: 'Otto Office GmbH', partnerIban: null, reference: 'OO-2026-Q4', purpose: 'Büromaterial Q4', matchedStatus: 'unmatched' },
|
||||
]
|
||||
for (const t of bankTx) {
|
||||
await db.bankTransaction.create({ data: t })
|
||||
}
|
||||
|
||||
// --- Accounting: VAT returns (Umsatzsteuer-Voranmeldung) -----------------
|
||||
// Past 3 months computed from invoices (mocked with realistic Kennzahlen)
|
||||
const vatReturns = [
|
||||
{ periodYear: 2026, periodMonth: 7, periodType: 'monthly', status: 'accepted', k81StandardNetCents: CENTS(4800), k86ReducedNetCents: 0, k66TaxFreeCents: 0, k83TaxStandardCents: CENTS(912), k65TaxReducedCents: 0, k37InputTaxCents: CENTS(180), outputTaxCents: CENTS(912), inputTaxCents: CENTS(180), payableCents: CENTS(732), submittedAt: daysFromNow(-85), elsterRef: 'ERiC-2026-07-NORDLICHT-0001', acceptedAt: daysFromNow(-83) },
|
||||
{ periodYear: 2026, periodMonth: 8, periodType: 'monthly', status: 'accepted', k81StandardNetCents: CENTS(6200), k86ReducedNetCents: 0, k66TaxFreeCents: 0, k83TaxStandardCents: CENTS(1178), k65TaxReducedCents: 0, k37InputTaxCents: CENTS(240), outputTaxCents: CENTS(1178), inputTaxCents: CENTS(240), payableCents: CENTS(938), submittedAt: daysFromNow(-55), elsterRef: 'ERiC-2026-08-NORDLICHT-0002', acceptedAt: daysFromNow(-53) },
|
||||
{ periodYear: 2026, periodMonth: 9, periodType: 'monthly', status: 'accepted', k81StandardNetCents: CENTS(5400), k86ReducedNetCents: 0, k66TaxFreeCents: 0, k83TaxStandardCents: CENTS(1026), k65TaxReducedCents: 0, k37InputTaxCents: CENTS(310), outputTaxCents: CENTS(1026), inputTaxCents: CENTS(310), payableCents: CENTS(716), submittedAt: daysFromNow(-25), elsterRef: 'ERiC-2026-09-NORDLICHT-0003', acceptedAt: daysFromNow(-23) },
|
||||
// current month — computed but not yet submitted
|
||||
{ periodYear: 2026, periodMonth: 10, periodType: 'monthly', status: 'computed', k81StandardNetCents: CENTS(7200), k86ReducedNetCents: 0, k66TaxFreeCents: 0, k83TaxStandardCents: CENTS(1368), k65TaxReducedCents: 0, k37InputTaxCents: CENTS(420), outputTaxCents: CENTS(1368), inputTaxCents: CENTS(420), payableCents: CENTS(948) },
|
||||
]
|
||||
for (const v of vatReturns) {
|
||||
await db.vatReturn.create({ data: v })
|
||||
}
|
||||
|
||||
// --- Dunning runs (Mahnwesen) ---------------------------------------------
|
||||
// the overdue TechStart invoice RE-2026-0036 got a level 1 dunning
|
||||
const overdueInv = await db.invoice.findFirst({ where: { status: 'overdue' } })
|
||||
if (overdueInv) {
|
||||
await db.dunningRun.create({ data: { invoiceId: overdueInv.id, level: 1, status: 'sent', feesCents: CENTS(5), sentAt: daysFromNow(-7) } })
|
||||
}
|
||||
|
||||
// audit entries for accounting
|
||||
await db.auditLog.create({ data: { actor: 'nora@nordlicht.digital', action: 'vat.compute', entity: 'VatReturn', entityId: 'seed', after: '{"period":"2026-10","payable":94800}', hash: 'sha256:' + Math.random().toString(36).slice(2) } })
|
||||
await db.auditLog.create({ data: { actor: 'nora@nordlicht.digital', action: 'vat.submit:elster', entity: 'VatReturn', entityId: 'seed', after: '{"elsterRef":"ERiC-2026-09","status":"accepted"}', hash: 'sha256:' + Math.random().toString(36).slice(2) } })
|
||||
await db.auditLog.create({ data: { actor: 'nora@nordlicht.digital', action: 'bank.import:camt053', entity: 'BankTransaction', entityId: 'seed', after: '{"count":10,"batch":"camt-2026-10"}', hash: 'sha256:' + Math.random().toString(36).slice(2) } })
|
||||
await db.auditLog.create({ data: { actor: 'nora@nordlicht.digital', action: 'bank.match:invoice', entity: 'BankTransaction', entityId: 'seed', after: '{"invoiceId":"matched","status":"matched"}', hash: 'sha256:' + Math.random().toString(36).slice(2) } })
|
||||
|
||||
console.log('Seed complete.')
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export type ViewId =
|
||||
| 'contracts'
|
||||
| 'expenses'
|
||||
| 'finance'
|
||||
| 'accounting'
|
||||
| 'audit'
|
||||
| 'reporting'
|
||||
| 'settings'
|
||||
|
||||
Reference in new issue
Block a user