import React, { useState } from "react"; import { formatCHF, formatDate, exportBuchhaltungCSV } from "../utils.js"; import { Header, StatusBadge } from "../components/UI.jsx"; import { MahnModal } from "./Protocols.jsx"; import { ReceiptViewer } from "./Expenses.jsx"; export default function Accounting({ data, update, setView, setPrintContent }) { const mwstRate = data.settings.mwstRate || 8.1; const currentYear = new Date().getFullYear().toString(); const [filterYear, setFilterYear] = useState(currentYear); const availableYears = Array.from(new Set([ ...data.invoices.map(i => (i.date || "").slice(0, 4)), ...(data.expenses || []).map(e => (e.date || "").slice(0, 4)), ].filter(Boolean))).sort().reverse(); if (!availableYears.includes(currentYear)) availableYears.unshift(currentYear); // Gefilterte Daten const invoices = data.invoices.filter(i => !filterYear || (i.date || "").startsWith(filterYear)); const expenses = (data.expenses || []).filter(e => !filterYear || (e.date || "").startsWith(filterYear)); const lohnEntries = (data.lohnEntries || []).filter(l => !filterYear || l.monat.startsWith(filterYear)); // Einnahmen const totalInvoiced = invoices.reduce((s, i) => s + (i.sub || 0), 0); const totalTax = invoices.reduce((s, i) => s + (i.tax || 0), 0); // Akonto-MwSt ist erst bei Schlussrechnung steuerrelevant — separat ausweisen const akontoInvoices = invoices.filter(i => i.invoiceKind === "akonto"); const akontoTax = akontoInvoices.reduce((s, i) => s + (i.tax || 0), 0); const akontoSub = akontoInvoices.reduce((s, i) => s + (i.sub || 0), 0); const taxWithoutAkonto = totalTax - akontoTax; const totalPaid = invoices.filter(i => i.status === "bezahlt").reduce((s, i) => s + (i.total || 0), 0); const totalOpen = invoices.filter(i => i.status === "gesendet" || i.status === "entwurf").reduce((s, i) => s + (i.total || 0), 0); const totalOverdue = invoices.filter(i => i.status === "überfällig").reduce((s, i) => s + (i.total || 0), 0); const totalDraftSub = invoices.filter(i => i.status === "entwurf").reduce((s, i) => s + (i.sub || 0), 0); const totalBilledSub = totalInvoiced - totalDraftSub; // Ausgaben Spesen const totalExpBrutto = expenses.reduce((s, e) => s + (e.amount || 0), 0); const totalExpNet = expenses.reduce((s, e) => { const net = e.inclMwst ? e.amount / (1 + (e.mwstRate || 0) / 100) : e.amount; return s + net; }, 0); const totalExpTax = totalExpBrutto - totalExpNet; // Interne Ausgaben const internalExpenses = (data.internalExpenses || []).filter(e => !filterYear || (e.date || "").startsWith(filterYear)); const totalIntExpBrutto = internalExpenses.reduce((s, e) => s + (e.amount || 0), 0); const totalIntExpNet = internalExpenses.reduce((s, e) => { const net = e.inclMwst ? e.amount / (1 + (e.mwstRate || 0) / 100) : e.amount; return s + net; }, 0); const totalIntExpTax = totalIntExpBrutto - totalIntExpNet; // Personalaufwand (abgeschlossene Lohnabrechnungen — Auszahlung an MA + AG-Abgaben) const totalLoehne = lohnEntries.reduce((s, l) => s + (l.auszahlung || 0), 0); const totalLoehneAGAbgaben = lohnEntries.reduce((s, l) => s + (l.bvgAG || 0), 0); const totalLoehneGesamt = totalLoehne + totalLoehneAGAbgaben; const totalLoehneAnzahl = lohnEntries.length; const totalAusgaben = totalExpNet + totalIntExpNet + totalLoehneGesamt; const result = totalInvoiced - totalAusgaben; // Überfällige Rechnungen für Mahnwesen const today = new Date().toISOString().slice(0, 10); const [mahnModal, setMahnModal] = useState(null); const [mahnMode, setMahnMode] = useState("new"); const [mahnSentDate, setMahnSentDate] = useState(new Date().toISOString().slice(0, 10)); const [receiptView, setReceiptView] = useState(null); const overdueInvoices = data.invoices.filter(i => (i.status === "gesendet" || i.status === "überfällig") && i.dueDate && i.dueDate < today ).sort((a, b) => (a.dueDate || "").localeCompare(b.dueDate || "")); const sendReminder = (inv) => { const reminders = inv.reminders || []; setMahnMode(reminders.length === 0 ? "new" : "reprint"); setMahnSentDate(new Date().toISOString().slice(0, 10)); setMahnModal({ inv }); }; return (
} /> {/* KPI-Karten */}
{[ { label: "UMSATZ NETTO", value: formatCHF(totalInvoiced), sub: totalDraftSub > 0 ? `${invoices.length} Rechnungen — davon ${formatCHF(totalDraftSub)} erwartet` : `${invoices.length} Rechnungen`, color: "#2d6a4f" }, { label: "DAVON BEZAHLT", value: formatCHF(totalPaid), sub: "eingegangen", color: "#2d6a4f" }, { label: "OFFEN / ÜBERFÄLLIG", value: formatCHF(totalOpen + totalOverdue), sub: `${overdueInvoices.length} überfällig`, color: totalOverdue > 0 ? "#8a1a1a" : "#7a6a00" }, { label: "AUSGABEN TOTAL", value: formatCHF(totalAusgaben), sub: `Spesen + ${totalLoehneAnzahl} Lohnabrechnungen (inkl. AG-Abgaben)`, color: "#555" }, ].map(c => (
{c.label}
{c.value}
{c.sub}
))}
{/* Ergebnis-Übersicht */}
JAHRESERGEBNIS {filterYear || "GESAMT"}
{[ { label: "Einnahmen (Netto)", value: totalInvoiced, bold: false }, totalDraftSub > 0 && { label: "→ Davon fakturiert", value: totalBilledSub, note: true }, totalDraftSub > 0 && { label: "→ Davon erwartet (Entwürfe)", value: totalDraftSub, note: true, expected: true }, { label: "Spesen (Netto)", value: -totalExpNet, bold: false }, { label: "Interne Ausgaben (Netto)", value: -totalIntExpNet, bold: false }, { label: "Personalaufwand (Löhne)", value: -totalLoehne, bold: false }, totalLoehneAGAbgaben > 0 && { label: "→ AG-Sozialabgaben (PK/BVG)", value: -totalLoehneAGAbgaben, note: true }, { label: "Ergebnis vor MWST", value: result, bold: true, sep: true }, { label: `MWST auf Einnahmen (${mwstRate}%, excl. Akonto)`, value: taxWithoutAkonto, bold: false, small: true }, akontoTax > 0 && { label: `↳ Akonto-MWST ausstehend (bei Schlussrechn.)`, value: akontoTax, bold: false, small: true, pending: true }, { label: "Vorsteuer (Spesen + Ausgaben)", value: -(totalExpTax + totalIntExpTax), bold: false, small: true }, { label: "MWST-Schuld (excl. Akonto)", value: taxWithoutAkonto - totalExpTax - totalIntExpTax, bold: true, small: true }, ].filter(Boolean).map((row, i) => (
{row.label} {row.expected && noch nicht fakturiert} {row.pending && ausstehend} {formatCHF(row.value)}
))}
{/* Monatsumsatz */}
MONATSUMSATZ {filterYear || new Date().getFullYear()}
{(() => { const year = filterYear || String(new Date().getFullYear()); const months = ["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"]; const monthData = months.map((label, i) => { const key = `${year}-${String(i + 1).padStart(2, "0")}`; const invs = data.invoices.filter(inv => (inv.date || "").startsWith(key)); const paid = invs.filter(inv => inv.status === "bezahlt").reduce((s, inv) => s + (inv.sub || 0), 0); const open = invs.filter(inv => inv.status === "gesendet" || inv.status === "überfällig").reduce((s, inv) => s + (inv.sub || 0), 0); const draft = invs.filter(inv => inv.status === "entwurf").reduce((s, inv) => s + (inv.sub || 0), 0); return { label, key, paid, open, draft, total: paid + open + draft }; }); const maxVal = Math.max(...monthData.map(m => m.total), 1); const yearTotal = monthData.reduce((s, m) => s + m.total, 0); const currentMonth = new Date().toISOString().slice(0, 7); return ( <>
Total {formatCHF(yearTotal)} · {data.invoices.filter(i => (i.date||"").startsWith(year)).length} Rechnungen
{monthData.map(m => (
{m.total === 0 ? (
) : ( <> {m.draft > 0 &&
} {m.open > 0 &&
} {m.paid > 0 &&
} )}
))}
{monthData.map(m => (
{m.label}
))}
{[ { color: "#2d6a4f", label: "Bezahlt" }, { color: "#b07848", label: "Ausstehend" }, { color: "#ccc", label: "Entwurf" }, ].map(l => (
{l.label}
))}
); })()}
{/* Mahnwesen */}
MAHNWESEN — ÜBERFÄLLIGE RECHNUNGEN {overdueInvoices.length === 0 && ✓ Keine überfälligen Rechnungen}
{overdueInvoices.length > 0 && ( {overdueInvoices.map(inv => { const client = ((data.persons||[]).filter(p=>p.isAuftraggeber)).find(c => c.id === inv.clientId); const daysPast = Math.floor((new Date() - new Date(inv.dueDate)) / 86400000); const reminders = inv.reminders || []; const nextNr = reminders.length + 1; const mahnLabel = nextNr === 1 ? "Zahlungserinnerung" : `${nextNr}. Mahnung`; const mahnColor = nextNr >= 3 ? "#8a1a1a" : nextNr === 2 ? "#b5621e" : "#7a6a00"; return ( ); })}
Nr.KundeFällig seitMahnungenBetrag
{inv.number} {client?.name || "—"} 30 ? "#8a1a1a" : "#b5621e", fontWeight: 500 }}> {formatDate(inv.dueDate)} ({daysPast} Tage) {reminders.length === 0 ? ( Keine ) : (
{reminders.map((r, i) => ( {i === 0 ? "Erinnerung" : `${i + 1}. Mahnung`} · {formatDate(r.date)} ))}
)}
{formatCHF(inv.total)}
)}
{/* Letzte Rechnungen */}
LETZTE RECHNUNGEN
{[...data.invoices].sort((a, b) => (b.date || "").localeCompare(a.date || "")).slice(0, 6).map(inv => { const client = ((data.persons||[]).filter(p=>p.isAuftraggeber)).find(c => c.id === inv.clientId); return ( ); })}
Nr.KundeDatumBetragStatus
{inv.number} {client?.name || "—"} {formatDate(inv.date)} {formatCHF(inv.total)}
{/* Spesen & Belege */} {expenses.length > 0 && (
SPESEN {filterYear || ""} {expenses.filter(e => e.receiptData).length} von {expenses.length} mit Beleg
{[...expenses].sort((a, b) => (b.date || "").localeCompare(a.date || "")).map(e => { const emp = (data.employees || []).find(em => em.id === e.employeeId); const expStatus = e.status || "offen"; const statusColors = { offen: "#b07848", genehmigt: "#2d6a4f", "auf nächsten Lohn": "#1a4e8a", ausbezahlt: "#2d6a4f" }; return ( ); })}
Datum Kategorie Beschreibung Mitarbeiter Brutto Beleg Status
{formatDate(e.date)} {e.category} {e.description || "—"} {emp?.name || "—"} {formatCHF(e.amount)} {e.receiptData ? ( ) : ( )} {expStatus.charAt(0).toUpperCase() + expStatus.slice(1)}
{expenses.length} Einträge {formatCHF(totalExpBrutto)}
)} {/* Mahnung-Dialog */} {mahnModal && ( setMahnModal(null)} mahnMode={mahnMode} setMahnMode={setMahnMode} mahnSentDate={mahnSentDate} setMahnSentDate={setMahnSentDate} /> )} setReceiptView(null)} />
); }