import React, { useState, useEffect } from "react"; import { SIA_PHASES, PROJECT_TYPES, STATUS_COLORS } from "../constants.js"; import { calcSIAHours, calcManualHours, formatCHF, formatDate, formatHours, formatSenderAddress, isQRIban, generateQRReference, formatIban, buildPdfName, sanitizeHtml } from "../utils.js"; import { StudioLogo } from "../components/UI.jsx"; export function PrintView({ content, onClose, settings }) { const triggerPrint = async () => { const pdfName = buildPdfName(settings?.pdfNameFormat, content, settings); const prevTitle = document.title; document.title = pdfName; try { if (window.__TAURI_INTERNALS__) { const { getCurrentWebviewWindow } = await import("@tauri-apps/api/webviewWindow"); await getCurrentWebviewWindow().print(); } else { window.print(); } } catch { window.print(); } finally { setTimeout(() => { document.title = prevTitle; }, 2000); } }; useEffect(() => { if (!settings.autoPrint) return; const timer = setTimeout(() => triggerPrint(), 400); return () => clearTimeout(timer); }, [settings.autoPrint]); const mTop = settings?.pageMarginTop ?? 20; const mBottom = settings?.pageMarginBottom ?? 20; const mLeft = settings?.pageMarginLeft ?? 20; const mRight = settings?.pageMarginRight ?? 20; const isQrOnly = content.type === "qrbill"; return (
{content.type === "invoice" && } {content.type === "invoice+qr" && ( <>
)} {content.type === "letter" && } {content.type === "projectDetail" && } {content.type === "projectsOverview" && } {content.type === "qrbill" && } {content.type === "quote" && } {content.type === "buchhaltung" && } {content.type === "lohn" && } {content.type === "lieferschein" && } {content.type === "studioBudget" && } {content.type === "protokoll" && } {content.type === "mitarbeiterOverview" && } {content.type === "timeReport" && }
); } export function InvoicePrint({ inv, client, settings }) { return ( <>
{formatSenderAddress(settings)}
{settings.email} · {settings.phone}
RECHNUNG
Nr. {inv.number}
RECHNUNG AN
{client?.name || "—"}
{(() => { const contact = inv.contactId ? (client?.contacts || []).find(ct => ct.id === inv.contactId) : null; if (contact) return
z.H. {contact.name}{contact.position ? `, ${contact.position}` : ""}
; return null; })()} {client?.street &&
{client.street}
} {(client?.zip || client?.city) &&
{[client.zip, client.city].filter(Boolean).join(" ")}
} {!client?.street && client?.address &&
{client.address}
}
Datum: {formatDate(inv.date)}
{inv.dueDate &&
Fällig: {formatDate(inv.dueDate)}
}
{(inv.items || []).some(it => (it.discount || 0) > 0) && ( )} {(inv.items || []).map((item, i) => { const lineTotal = item.qty * item.price; const lineDisc = (item.discount || 0) > 0 ? lineTotal * (item.discount / 100) : 0; const hasAnyDiscount = (inv.items || []).some(it => (it.discount || 0) > 0); return ( {hasAnyDiscount && ( )} ); })}
BESCHREIBUNG MENGE PREISRABATTTOTAL
{item.desc} {item.qty} {formatCHF(item.price)} {(item.discount || 0) > 0 ? `−${item.discount}%` : "—"} {formatCHF(lineTotal - lineDisc)}
{(inv.globalDisc || 0) > 0 && <>
Zwischentotal{formatCHF((inv.items || []).reduce((s, it) => { const l = it.qty * it.price; return s + l - ((it.discount||0) > 0 ? l*(it.discount/100) : 0); }, 0))}
{inv.discountLabel || "Rabatt"}−{formatCHF(inv.globalDisc)}
} {inv.mwst && <>
Netto{formatCHF(inv.sub)}
MWST {inv.mwstRate || settings.mwstRate}%{formatCHF(inv.tax)}
}
Total{formatCHF(inv.total)}
{inv.notes &&
{inv.notes}
}
ZAHLUNG AUF
{settings.iban}
Referenz: {inv.number}
{settings.mwst}
); } export function LieferscheinPrint({ note, client, data, settings }) { const project = note.projectId ? (data?.projects || []).find(p => p.id === note.projectId) : null; const projectLabel = project?.name || note.projectManual || null; const addr = note.deliveryAddress || client?.address || ""; return ( <> {/* Kopf */}
{formatSenderAddress(settings)}
{settings.email} · {settings.phone}
LIEFERSCHEIN
{note.number || "—"}
Datum: {formatDate(note.date)}
{/* Empfänger & Projektinfo */}
LIEFERUNG AN
{client?.name || note.clientManual || "—"}
{client?.company &&
{client.company}
} {addr &&
{addr}
}
{projectLabel && (
PROJEKT
{projectLabel}
{project?.number &&
{project.number}
}
)}
{/* Positionen */} {(note.items || []).map((it, i) => ( ))}
POS. BESCHREIBUNG MENGE EINHEIT BEMERKUNG EMPFANGEN ✓
{i + 1} {it.desc || "—"} {it.qty} {it.unit} {it.note || ""}
{/* Notizen */} {note.notes && (
{note.notes}
)} {/* Unterschriften */}
ÜBERGABE – {settings.name}
Datum / Unterschrift
EMPFANG – {client?.name || note.clientManual || "Empfänger"}
Datum / Unterschrift
{/* Footer */}
{settings.name} · {formatSenderAddress(settings).split("\n")[0]}
{note.number}
); } export function StudioBudgetPrint({ snapshot, settings }) { const r = snapshot.results || {}; const rateOk = (r.currentRate || 0) >= (r.zielHonorar || 0); const b = snapshot.b || {}; const PRow = ({ label, value, bold, indent, color }) => (
{label} {value}
); return ( <> {/* Kopf */}
{formatSenderAddress(settings)}
BÜROBUDGET
{snapshot.name}
Erstellt: {snapshot.savedAt ? new Date(snapshot.savedAt).toLocaleDateString("de-CH", { day: "numeric", month: "long", year: "numeric" }) : "—"}
{/* Kostenstruktur */}
KOSTENSTRUKTUR / JAHR
Gesamtkosten {formatCHF(r.gesamtKosten || 0)}
{/* Jahresstunden & Kernresultat */}
STUNDENANALYSE
SELBSTKOSTEN/H
{formatCHF(Math.round(r.selbstkosten || 0))}
ZIEL-HONORAR/H
{formatCHF(Math.round(r.zielHonorar || 0))}
inkl. {b.zielMarge || 25}% Marge
{rateOk ? "✓ Aktueller Ansatz ausreichend" : "⚠ Aktueller Ansatz zu tief"}
Aktuell: {formatCHF(r.currentRate || 0)}/h · Ziel: {formatCHF(Math.round(r.zielHonorar || 0))}/h · Differenz: {rateOk ? "+" : ""}{formatCHF(Math.round((r.currentRate || 0) - (r.zielHonorar || 0)))}
{/* Personal-Detail */} {(snapshot.empSnapshot || []).length > 0 && (
PERSONAL
{snapshot.empSnapshot.filter(r => r.aktiv).map((r, i) => ( ))}
Mitarbeiter Jahreskosten Jahresstunden
{r.name} {formatCHF(r.kosten)} {r.stunden}h
)} {/* Fixkosten-Detail */} {(snapshot.fixSnapshot || []).length > 0 && (
FIXKOSTEN
{snapshot.fixSnapshot.map((r, i) => ( ))}
Posten CHF/Jahr
{r.label} {formatCHF(r.amount)}
)} {/* Footer */}
{settings.name}
Bürobudget · {snapshot.name}
); } export function ProtokollPrint({ protokoll, data, settings }) { const p = protokoll; const proj = data?.projects?.find(x => x.id === p.projectId); const projLabel = proj?.name || p.projectManual || null; const today = new Date().toLocaleDateString("de-CH", { day: "numeric", month: "long", year: "numeric" }); const statusLabels = { anwesend: "A", entschuldigt: "E", abwesend: "Ab", eingeladen: "Eingeladen" }; const statusColors = { anwesend: "#2d6a4f", entschuldigt: "#b5621e", abwesend: "#8a1a1a", eingeladen: "#1a4e8a" }; const allTasks = (p.traktanden || []).flatMap(t => (t.items || []).filter(it => it.type === "aufgabe") .map(it => ({ ...it, tNr: t.nr, tTitle: t.title })) ); const allBeschluesse = (p.traktanden || []).flatMap(t => (t.items || []).filter(it => it.type === "beschluss") .map(it => ({ ...it, tNr: t.nr, tTitle: t.title })) ); return ( <> {/* Kopf */}
{formatSenderAddress(settings)}
PROTOKOLL
{p.nummer}
{/* Titel & Meta */}
{p.title || "Protokoll"}
{[ { label: "TYP", value: p.type }, { label: "DATUM", value: p.date ? `${formatDate(p.date)}${p.time ? `, ${p.time}${p.endTime ? `–${p.endTime}` : ""} Uhr` : ""}` : "—" }, { label: "ORT", value: p.location || "—" }, { label: "PROJEKT", value: projLabel || "—" }, ].map(m => (
{m.label}
{m.value}
))}
{/* Teilnehmer */} {(p.participants || []).length > 0 && (
TEILNEHMER
{(p.participants || []).map((tn, i) => ( ))}
Name Funktion Status
{tn.name} {tn.role || "—"} {statusLabels[tn.status] || tn.status}
)} {/* Traktanden */} {(p.traktanden || []).map(t => { const hasContent = t.title || (t.items || []).length > 0; if (!hasContent) return null; return (
{t.nr} {t.title || "—"}
{(t.items || []).map((item, ii) => { const icons = { info: "ℹ", beschluss: "✅", aufgabe: "📌" }; const colors = { info: "#1a4e8a", beschluss: "#2d6a4f", aufgabe: "#b5621e" }; return (
{icons[item.type]}
{item.text || "—"}
{item.type === "beschluss" && item.date && (
Beschluss vom {formatDate(item.date)}
)} {item.type === "aufgabe" && (
{item.responsible && → {item.responsible}} {item.dueDateType === "kw" ? (item.dueKW ? `KW ${item.dueKW}/${item.dueYear || ""}` : "—") : item.dueDate ? formatDate(item.dueDate) : "—"} {item.status}
)}
); })}
); })} {/* Aufgabenliste */} {allTasks.length > 0 && (
AUFGABEN-ÜBERSICHT
{allTasks.map((t, i) => ( ))}
Aufgabe Verantwortlich Fälligkeit Status
{t.tNr}.{t.text || "—"} {t.responsible || "—"} {t.dueDateType === "kw" ? (t.dueKW ? `KW ${t.dueKW}/${t.dueYear || ""}` : "—") : t.dueDate ? formatDate(t.dueDate) : "—"} {t.status}
)} {/* Nächste Sitzung */} {(p.nextDate || p.verteiler) && (
{p.nextDate &&
Nächste Sitzung:{formatDate(p.nextDate)}
} {p.verteiler &&
Verteiler:{p.verteiler}
}
)} {/* Unterschriften */}
PROTOKOLLFÜHRUNG
Datum / Unterschrift
GENEHMIGUNG
Datum / Unterschrift
{/* Footer */}
{settings.name} · {p.nummer}
Erstellt {today}{p.verteiler ? ` · Verteiler: ${p.verteiler}` : ""}
); } export function LetterPrint({ client, subject, body, isHtml, settings }) { return ( <>
{formatSenderAddress(settings)}
{settings.email}
{settings.phone}
{client ? (<>
{client.name}
{client.company &&
{client.company}
} {client.street &&
{client.street}
} {(client.zip || client.city) &&
{[client.zip, client.city].filter(Boolean).join(" ")}
} {!client.street && !client.city && client.address &&
{client.address}
} ) :
[Empfänger]
}
{settings.city || (settings.address || "").split("\n").pop().replace(/^\d{4,5}\s*/, "").trim() || "Zürich"}, {new Date().toLocaleDateString("de-CH", { day: "numeric", month: "long", year: "numeric" })}
{subject &&
{subject}
} {isHtml ?
:
{body}
}
{settings.name}
); } export function ProjectDetailPrint({ content, settings }) { const { project, client, entries, phaseStats, unassignedMins, totalMinutes, totalAmount, billingType, invoices, data } = content; return ( <>
{formatSenderAddress(settings)}
PROJEKTREPORT
{new Date().toLocaleDateString("de-CH")}
{(project.category || "PROJEKT").toUpperCase()}
{project.name}
{client &&
{client.name}{client.company ? ` · ${client.company}` : ""}
} {project.description &&
{project.description}
}
STUNDEN TOTAL
{formatHours(totalMinutes)}
{billingType === "stundensatz" ? "AUFWAND" : "PAUSCHALE"}
{formatCHF(totalAmount)}
EINTRÄGE
{entries.length}
{phaseStats.length > 0 && (
AUFWAND PRO SIA-PHASE · HAUPTAUFTRAG
{phaseStats.map(ps => (
{ps.label} {formatHours(ps.minutes)} · {ps.percent.toFixed(1)}%
))} {unassignedMins > 0 &&
Ohne Phasen-Zuordnung: {formatHours(unassignedMins)}
}
)} {(project.positions || []).filter(pos => { const posEntries = entries.filter(e => e.positionId === pos.code); return posEntries.length > 0 || (pos.enabledPhases || []).length > 0; }).map(pos => { const posEntries = entries.filter(e => e.positionId === pos.code); const posMins = posEntries.reduce((s, e) => s + (e.minutes || 0), 0); const allPhaseIds = [...new Set([...(pos.enabledPhases || []), ...posEntries.map(e => e.phaseId).filter(Boolean)])]; const linkedQ = pos.quoteId ? (data?.quotes || []).find(q => q.id === pos.quoteId) : null; return (
↪ {pos.code}{pos.label ? ` · ${pos.label}` : ""}{linkedQ ? ` · ${linkedQ.number}` : ""} {formatHours(posMins)}
{allPhaseIds.length > 0 ? allPhaseIds.map(phId => { const ph = SIA_PHASES.find(p => p.id === phId); if (!ph) return null; const phMins = posEntries.filter(e => e.phaseId === phId).reduce((s, e) => s + (e.minutes || 0), 0); const pct = posMins > 0 ? (phMins / posMins) * 100 : 0; return (
{ph.label} {formatHours(phMins)} · {pct.toFixed(1)}%
); }) : ( posMins === 0 ? null :
Keine Phasen-Unterteilung · {formatHours(posMins)}
)}
); })} {entries.length > 0 && (
ZEITEINTRÄGE
{entries.map(e => { const phase = SIA_PHASES.find(p => p.id === e.phaseId); return ( ); })}
DATUM PHASE TÄTIGKEIT DAUER
{formatDate(e.date)} {phase?.id || "—"} {e.description || "—"} {formatHours(e.minutes)}
)} {invoices && invoices.length > 0 && (
RECHNUNGEN
{invoices.map(inv => ( ))}
{inv.number} {formatDate(inv.date)} {inv.status} {formatCHF(inv.total)}
)} ); } export function ProjectsOverviewPrint({ projects, data, settings }) { const projectMinutes = (id) => data.timeEntries.filter(e => e.projectId === id).reduce((s, e) => s + (e.minutes || 0), 0); const projectAmount = (p) => { const bType = p.billingType || p.type; if (bType === "stundensatz") return (projectMinutes(p.id) / 60) * p.hourlyRate; return p.budget || 0; }; const grouped = PROJECT_TYPES.map(cat => ({ category: cat, projects: projects.filter(p => (p.category || "Sonstiges") === cat), })).filter(g => g.projects.length > 0); const uncategorized = projects.filter(p => !p.category); if (uncategorized.length > 0) grouped.push({ category: "Ohne Kategorie", projects: uncategorized }); const totalMins = projects.reduce((s, p) => s + projectMinutes(p.id), 0); const totalAmount = projects.reduce((s, p) => s + projectAmount(p), 0); return ( <>
{formatSenderAddress(settings)}
PROJEKTÜBERSICHT
{new Date().toLocaleDateString("de-CH")}
Alle Projekte
{projects.length} Projekte · {formatHours(totalMins)} · {formatCHF(totalAmount)}
{grouped.map(g => (
{g.category.toUpperCase()} — {g.projects.length}
{g.projects.map(p => { const client = ((data.persons||[]).filter(p=>p.isAuftraggeber)).find(c => c.id === p.clientId); return ( ); })}
PROJEKT KUNDE STATUS STUNDEN BETRAG
{p.name} {client?.name || "—"} {p.status} {formatHours(projectMinutes(p.id))} {p.budgetHours > 0 && / {p.budgetHours}h} {formatCHF(projectAmount(p))}
))}
Total {formatHours(totalMins)} · {formatCHF(totalAmount)}
); } export function QRBillPrint({ inv, client, settings }) { const [qrSvg, setQrSvg] = useState(null); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; (async () => { try { const { SwissQRBill: SVG } = await import("swissqrbill/svg"); if (cancelled) return; const ibanClean = (settings.iban || "").replace(/\s/g, "").toUpperCase(); const hasQRIban = isQRIban(ibanClean); const data = { currency: "CHF", amount: inv.total, creditor: { name: settings.name || "—", address: settings.street || "—", zip: settings.zip || "", city: settings.city || "", account: ibanClean, country: settings.country || "CH", }, }; if (client) { data.debtor = { name: client.company || client.name || "—", address: client.street || "—", zip: client.zip || "", city: client.city || "", country: client.country || "CH", }; } if (hasQRIban) { data.reference = generateQRReference(inv.number); } if (inv.notes || inv.number) { data.message = `Rechnung ${inv.number}`; } const qr = new SVG(data); const svgString = typeof qr.toString === "function" ? qr.toString() : (qr.element?.outerHTML || qr.outerHTML || String(qr)); if (!cancelled) setQrSvg(svgString); } catch (err) { console.error("QR-Bill Fehler:", err); if (!cancelled) setError(err.message || "Fehler beim Generieren der QR-Rechnung"); } })(); return () => { cancelled = true; }; }, [inv, client, settings]); if (error) { return (
Fehler beim Generieren
{error}
Mögliche Ursachen: fehlende Empfänger-Adresse (Strasse, PLZ, Ort) oder ungültige IBAN.
); } if (!qrSvg) { return (
QR-Rechnung wird generiert…
); } return (
); } export function QuotePrint({ quote, client, settings }) { const taxRate = settings.mwstRate || 8.1; const roles = quote.quoteRoles || settings.roles || []; const siaCalc = quote.mode === "sia" && quote.sia ? calcSIAHours(quote.sia.baukosten, quote.sia.schwierigkeit, quote.sia.phases) : null; const manCalc = quote.mode === "manual" ? calcManualHours(quote.manualPhases || [], roles) : null; const stundenansatz = quote.sia?.stundenansatz || 0; return ( <>
{formatSenderAddress(settings)}
{settings.email} · {settings.phone}
HONORAROFFERTE
Nr. {quote.number}
OFFERTE AN
{client ? ( <> {client.company &&
{client.company}
}
{client.name || "—"}
{client.street &&
{client.street}
} {(client.zip || client.city) &&
{[client.zip, client.city].filter(Boolean).join(" ")}
} {!client.street && !client.city && client.address &&
{client.address}
} ) :
}
Datum: {formatDate(quote.date)}
{quote.validUntil &&
Gültig bis: {formatDate(quote.validUntil)}
}
{quote.notes &&
{quote.notes}
} {/* SIA-Modus */} {quote.mode === "sia" && siaCalc && ( <>
HONORARBERECHNUNG NACH SIA 102
Aufwandbestimmende Baukosten
{formatCHF(quote.sia.baukosten)}
Schwierigkeitsgrad n
{quote.sia.schwierigkeit}
Stundenansatz
CHF {stundenansatz}.–/h
{siaCalc.phases.map(ph => ( {ph.items.filter(it => it.enabled !== false && it.hours > 0).map((it, idx) => ( ))} ))}
TEILLEISTUNG % STUNDEN HONORAR
Phase {ph.id} · {ph.label}
{it.label} {it.pct}% {formatHours(Math.round(it.hours * 60))} {formatCHF(it.hours * stundenansatz)}
Total Phase {ph.id} {formatHours(Math.round(ph.hours * 60))} {formatCHF(ph.hours * stundenansatz)}
)} {/* Manueller Modus */} {quote.mode === "manual" && manCalc && ( <>
HONORARSCHÄTZUNG STUNDENAUFWAND
{roles.map(r => (
{r.id} {r.label} — CHF {r.rate}.–/h
))}
{roles.map(r => ( ))} {manCalc.phases.filter(p => p.totalHours > 0).map(ph => ( {roles.map(r => { const h = ph.roleDetails.find(rd => rd.id === r.id)?.hours || 0; return ; })} ))}
PHASE{r.id}Std Honorar
{ph.label}{h || "—"}{ph.totalHours} {formatCHF(ph.totalAmount)}
)} {/* Freier Modus */} {quote.mode === "free" && (quote.freeItems || []).length > 0 && ( <>
LEISTUNGEN / POSITIONEN
{(quote.freeItems || []).map((it, idx) => ( ))}
BESCHREIBUNG MENGE PREIS TOTAL
{it.desc || "—"} {it.qty} {formatCHF(it.price)} {formatCHF(it.qty * it.price)}
)} {/* Total */}
Netto {formatCHF(quote.sub)}
{quote.mwst && (
MWST {taxRate}% {formatCHF(quote.tax)}
)}
Offertsumme {formatCHF(quote.total)}
Diese Offerte ist unverbindlich und {quote.validUntil ? `gültig bis ${formatDate(quote.validUntil)}` : "zeitlich unbegrenzt gültig"}. {quote.mode !== "free" && " Honorar gemäss SIA-Ordnung 102."} Änderungen am Auftragsumfang können zu einer Anpassung führen.
); } export function LohnPrint({ entry, emp, data, monatLabel, settings }) { const spesen = (data.expenses || []).filter(e => e.lohnEntryId === entry.id); // Immer den gespeicherten Snapshot verwenden — nie live emp-Werte const s = entry.saetzeSnapshot || emp; const LRow = ({ label, betrag, satz, bold, sub, color, topBorder }) => ( {label} {satz || ""} {formatCHF(betrag)} ); return (
{/* Header: Arbeitgeber links, Arbeitnehmer rechts */}
{formatSenderAddress(settings).split("\n").map((l,i) =>
{l}
)} {settings.email &&
{settings.email}
}
LOHNABRECHNUNG
{entry.empSnapshot?.name || emp.name}
{emp.role &&
{emp.role}
} {emp.personalNr &&
Personal-Nr. {emp.personalNr}
} {emp.ahvNr &&
AHV-Nr. {emp.ahvNr}
} {emp.adresse &&
{emp.adresse}
} {emp.ort &&
{emp.ort}
}
{/* Periode */}
PERIODE
{monatLabel}
ABRECHNUNGSDATUM
{new Date(entry.createdAt).toLocaleDateString("de-CH")}
{emp.eintrittsdatum && (
EINTRITTSDATUM
{formatDate(emp.eintrittsdatum)}
)}
PENSUM
{s.pensum || 100}%
{/* Lohn-Tabelle */} {(s.pensum || 100) < 100 && entry.bruttoBase != null && entry.bruttoBase !== entry.brutto && ( )} {entry.dreizehnter > 0 && } {entry.bonusBetrag > 0 && } {/* Spacer */} {entry.qst > 0 && } {spesen.length > 0 && <> {spesen.map(s => )} }
POSITION SATZ BETRAG CHF
ABZÜGE ARBEITNEHMER
SPESENERSTATTUNG
{/* Überweisungsdetails */} {emp.lohnIban && (
ÜBERWEISUNG AUF
{formatIban(emp.lohnIban)}
{emp.name &&
{emp.name}{emp.ort ? ` · ${emp.ort}` : ""}
}
)} {/* Arbeitgeber-Anteile (informativ, klein) */}
ARBEITGEBERANTEILE (informativ)
{[ { label: "AHV/IV/EO", val: Math.round(entry.bruttoTotal * (s.ahvSatz ?? 5.3) / 100 * 100)/100 }, { label: "ALV", val: Math.round(entry.bruttoTotal * (s.alvSatz ?? 1.1) / 100 * 100)/100 }, { label: "BVG/PK (AG)", val: Math.round(entry.bruttoTotal * (s.bvgSatz ?? 8.0) / 100 * 100)/100 }, { label: "UVG BU", val: Math.round(entry.bruttoTotal * 0.5 / 100 * 100)/100 }, ].map(r => (
{r.label}
{formatCHF(r.val)}
))}
Lohnabrechnung gemäss OR Art. 323b · {settings.name}{settings.mwst ? ` · ${settings.mwst}` : ""}
); } export function BuchhaltungPrint({ data, filterYear, settings }) { const mwstRate = settings.mwstRate || 8.1; const invoices = [...data.invoices] .filter(i => !filterYear || (i.date || "").startsWith(filterYear)) .sort((a, b) => (a.date || "").localeCompare(b.date || "")); const expenses = [...(data.expenses || [])] .filter(e => !filterYear || (e.date || "").startsWith(filterYear)) .sort((a, b) => (a.date || "").localeCompare(b.date || "")); const loehne = [...(data.lohnEntries || [])] .filter(l => !filterYear || l.monat.startsWith(filterYear)) .sort((a, b) => a.monat.localeCompare(b.monat)); const totalInvoicedNet = invoices.reduce((s, i) => s + (i.sub || 0), 0); const totalInvoicedTax = invoices.reduce((s, i) => s + (i.tax || 0), 0); 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; const totalLoehne = loehne.reduce((s, l) => s + (l.auszahlung || 0), 0); const totalAusgaben = totalExpNet + totalLoehne; const thStyle = { textAlign: "left", padding: "6px 0", fontSize: 8, letterSpacing: "0.1em", color: "#888", fontWeight: 500, borderBottom: "1px solid #1a1a18" }; const thR = { ...thStyle, textAlign: "right" }; const tdStyle = { padding: "5px 0", fontSize: 9, borderBottom: "1px solid #f0f0f0" }; const tdR = { ...tdStyle, textAlign: "right" }; return ( <> {/* Header */}
{formatSenderAddress(settings)}
BUCHHALTUNGSÜBERSICHT
{filterYear || "Alle Jahre"}
{new Date().toLocaleDateString("de-CH")}
{/* Zusammenfassung */}
EINNAHMEN
{[ { label: "Umsatz (Netto)", value: totalInvoicedNet }, { label: `MWST ${mwstRate}%`, value: totalInvoicedTax, small: true }, { label: "Umsatz (Brutto)", value: totalInvoicedNet + totalInvoicedTax, bold: true }, ].map((r, i) => (
{r.label} {formatCHF(r.value)}
))}
AUSGABEN & ERGEBNIS
{[ { label: "Spesen / Ausgaben (Netto)", value: totalExpNet }, { label: "Vorsteuer", value: totalExpTax, small: true }, { label: `Personalaufwand / Löhne (${loehne.length})`, value: totalLoehne }, { label: "Ergebnis (Netto)", value: totalInvoicedNet - totalAusgaben, bold: true }, { label: "MWST-Schuld", value: totalInvoicedTax - totalExpTax, bold: true, small: true }, ].map((r, i) => (
{r.label} {formatCHF(r.value)}
))}
{/* Rechnungen */}
RECHNUNGEN ({invoices.length})
{invoices.map(inv => { const client = ((data.persons||[]).filter(p=>p.isAuftraggeber)).find(c => c.id === inv.clientId); const desc = (inv.items || []).map(it => it.desc).filter(Boolean).join(", "); return ( ); })}
NR. DATUM KUNDE BESCHREIBUNG NETTO MWST TOTAL STATUS
{inv.number} {formatDate(inv.date)} {client?.name || "—"} {desc || "—"} {formatCHF(inv.sub)} {inv.mwst ? formatCHF(inv.tax) : "—"} {formatCHF(inv.total)} {inv.status}
Total {formatCHF(totalInvoicedNet)} {formatCHF(totalInvoicedTax)} {formatCHF(totalInvoicedNet + totalInvoicedTax)}
{/* Spesen */}
SPESEN / AUSGABEN ({expenses.length})
{expenses.map(e => { const proj = data.projects.find(p => p.id === e.projectId); const net = e.inclMwst ? e.amount / (1 + (e.mwstRate || 0) / 100) : e.amount; const tax = e.amount - net; return ( ); })}
DATUM KATEGORIE PROJEKT BESCHREIBUNG NETTO VORSTEUER BRUTTO
{formatDate(e.date)} {e.category} {proj?.name || "—"} {e.description || "—"} {formatCHF(net)} {e.mwstRate > 0 ? formatCHF(tax) : "—"} {formatCHF(e.amount)}
Total {formatCHF(totalExpNet)} {formatCHF(totalExpTax)} {formatCHF(totalExpBrutto)}
{/* Personalaufwand / Löhne */} {loehne.length > 0 && (
PERSONALAUFWAND / LÖHNE ({loehne.length})
{loehne.map(l => ( ))}
MONAT MITARBEITER BRUTTO ABZÜGE AN NETTO SPESEN AUSZAHLUNG
{l.monat} {l.empSnapshot?.name || "—"} {formatCHF(l.bruttoTotal)} {formatCHF(l.totalAbzuege)} {formatCHF(l.netto)} {l.spesenTotal > 0 ? formatCHF(l.spesenTotal) : "—"} {formatCHF(l.auszahlung)}
Total Auszahlungen {formatCHF(totalLoehne)}
)}
{settings.name} · {settings.mwst} · Erstellt am {new Date().toLocaleDateString("de-CH")}
); } function MitarbeiterOverviewPrint({ employees, settings }) { const active = employees.filter(e => e.status !== "inaktiv"); const inactive = employees.filter(e => e.status === "inaktiv"); const Row = ({ label, value }) => value ? (
{label} {value}
) : null; const EmpCard = ({ emp }) => (
{emp.name}
{emp.role &&
{emp.role}
}
{emp.personalNr &&
#{emp.personalNr}
}
); return ( <>
{formatSenderAddress(settings)}
MITARBEITERÜBERSICHT
{new Date().toLocaleDateString("de-CH")}
Mitarbeiter
{employees.length} Mitarbeitende · {active.length} aktiv
{active.length > 0 && (
AKTIV — {active.length}
{active.map(e => )}
)} {inactive.length > 0 && (
INAKTIV — {inactive.length}
{inactive.map(e => )}
)} ); } function TimeReportPrint({ employee, entries, month, data, settings }) { const monthEntries = entries.filter(e => e.date.startsWith(month)) .sort((a, b) => a.date.localeCompare(b.date) || (a.startTime || "").localeCompare(b.startTime || "")); const projects = data.projects || []; const getProj = (id) => projects.find(p => p.id === id); const totalMins = monthEntries.reduce((s, e) => s + (e.minutes || 0), 0); const byProject = {}; monthEntries.forEach(e => { const k = e.projectId || "__none__"; if (!byProject[k]) byProject[k] = { proj: getProj(e.projectId), mins: 0 }; byProject[k].mins += e.minutes || 0; }); const monthLabel = new Date(month + "-01").toLocaleDateString("de-CH", { month: "long", year: "numeric" }); return ( <>
{formatSenderAddress(settings)}
STUNDENRAPPORT
{new Date().toLocaleDateString("de-CH")}
{employee?.name}
{monthLabel} · {formatHours(totalMins)}
{/* Zusammenfassung */}
ZUSAMMENFASSUNG PRO PROJEKT
{Object.values(byProject).sort((a, b) => b.mins - a.mins).map(({ proj, mins }) => (
{proj ? `${proj.number ? proj.number + " " : ""}${proj.name}` : "Kein Projekt"} {formatHours(mins)}
))}
Total{formatHours(totalMins)}
{/* Detailliste */}
EINZELEINTRÄGE
{["Datum", "Projekt", "Zeit", "Stunden", "Notiz"].map(h => ( ))} {monthEntries.map(e => { const proj = getProj(e.projectId); return ( ); })}
{h}
{formatDate(e.date)} {proj ? `${proj.number ? proj.number + " " : ""}${proj.name}` : "—"} {e.startTime && e.endTime ? `${e.startTime}–${e.endTime}` : "—"} {formatHours(e.minutes || 0)} {e.note || ""}
{monthEntries.length === 0 && (
Keine Einträge für diesen Monat
)} ); }