import React, { useState } from "react"; import { generateId, formatCHF, calcLohn } from "../utils.js"; import { Header, FormField } from "../components/UI.jsx"; export default function StudioBudget({ data, update, setView, setPrintContent }) { const employees = data.employees || []; const internalExpenses = data.internalExpenses || []; const currentYear = new Date().getFullYear(); const saved = data.settings.studioBudget || {}; // ── Hilfsfunktion: Auto-Werte für einen MA berechnen ────────── const calcEmpAuto = (emp, agZuschlag) => { const pensum = (emp.pensum || 100) / 100; const brutto12 = (emp.monatslohn || 0) * 12; const brutto13 = emp.dreizehnterLohn ? brutto12 + (emp.monatslohn || 0) : brutto12; const agAnteil = brutto13 * ((agZuschlag || 20) / 100); const totalKosten = brutto13 + agAnteil; const ferienWochen = emp.ferienWochen || data.settings.defaultFerienWochen || 5; const wochenstunden = (emp.wochenstunden || data.settings.defaultWochenstunden || 35) * pensum; const nettoWochen = 52 - ferienWochen - 1.5; const jahresStunden = Math.round(wochenstunden * nettoWochen); return { brutto13, agAnteil, totalKosten, jahresStunden }; }; // ── Hilfsfunktion: Auto-Werte für interne Ausgaben ─────────── const calcFixAuto = () => { const rows = []; internalExpenses.forEach(e => { const year = (e.date || "").slice(0, 4); const net = e.inclMwst ? e.amount / (1 + (e.mwstRate || 0) / 100) : e.amount; let annualized = net; if (e.recurring) { if (e.recurringInterval === "monatlich") annualized = net * 12; else if (e.recurringInterval === "quartalsweise") annualized = net * 4; } else if (year !== String(currentYear)) return; rows.push({ id: e.id, label: e.description || e.category, cat: e.category, amount: annualized }); }); return rows; }; // ── State: ein Eintrag pro MA, pro Fixkostenposten, plus globale Parameter ── const initState = () => { const sv = saved; const agZuschlag = sv.agZuschlag ?? 20; // Mitarbeiter-Zeilen const empRows = employees.map(emp => { const auto = calcEmpAuto(emp, agZuschlag); const saved_emp = (sv.empRows || []).find(r => r.id === emp.id) || {}; return { id: emp.id, aktiv: saved_emp.aktiv ?? true, kostenOverride: saved_emp.kostenOverride ?? false, kostenManual: saved_emp.kostenManual ?? auto.totalKosten, stundenOverride: saved_emp.stundenOverride ?? false, stundenManual: saved_emp.stundenManual ?? auto.jahresStunden, }; }); // Fixkosten-Zeilen (aus internen Ausgaben) const autoFix = calcFixAuto(); const fixRows = autoFix.map(f => { const saved_fix = (sv.fixRows || []).find(r => r.id === f.id) || {}; return { id: f.id, aktiv: saved_fix.aktiv ?? true, amountOverride: saved_fix.amountOverride ?? false, amountManual: saved_fix.amountManual ?? f.amount, }; }); // Zusätzliche manuelle Fixkostenposten const extraRows = (sv.extraRows || []); // Manuelle Einnahmen + Rechnungs-Toggles const extraIncome = sv.extraIncome || []; return { agZuschlag, empRows, fixRows, extraRows, // { id, label, amount, aktiv } extraIncome, // { id, label, amount, aktiv } inkludiereRechnungen: sv.inkludiereRechnungen ?? true, inkludiereEntwuerfe: sv.inkludiereEntwuerfe ?? true, inkludiereOfferten: sv.inkludiereOfferten ?? false, inkludiereOffertEntwuerfe: sv.inkludiereOffertEntwuerfe ?? false, reserve: sv.reserve ?? 10, produktivQuote: sv.produktivQuote ?? 70, zielMarge: sv.zielMarge ?? 25, rateOverride: sv.rateOverride ?? false, rateManual: sv.rateManual ?? (data.settings.defaultHourlyRate || 120), }; }; const [b, setB] = useState(initState); const [newExtra, setNewExtra] = useState({ label: "", amount: 0 }); const [newIncome, setNewIncome] = useState({ label: "", amount: 0 }); const setField = (k, v) => setB(prev => ({ ...prev, [k]: v })); const setEmpRow = (id, changes) => setB(prev => ({ ...prev, empRows: prev.empRows.map(r => r.id === id ? { ...r, ...changes } : r), })); const setFixRow = (id, changes) => setB(prev => ({ ...prev, fixRows: prev.fixRows.map(r => r.id === id ? { ...r, ...changes } : r), })); const addExtra = () => { if (!newExtra.label.trim() || !newExtra.amount) return; setB(prev => ({ ...prev, extraRows: [...prev.extraRows, { id: generateId(), label: newExtra.label, amount: newExtra.amount, aktiv: true }] })); setNewExtra({ label: "", amount: 0 }); }; const setExtraRow = (id, changes) => setB(prev => ({ ...prev, extraRows: prev.extraRows.map(r => r.id === id ? { ...r, ...changes } : r), })); const delExtra = (id) => setB(prev => ({ ...prev, extraRows: prev.extraRows.filter(r => r.id !== id) })); const addIncome = () => { if (!newIncome.label.trim() || !newIncome.amount) return; setB(prev => ({ ...prev, extraIncome: [...(prev.extraIncome || []), { id: generateId(), label: newIncome.label, amount: newIncome.amount, aktiv: true }] })); setNewIncome({ label: "", amount: 0 }); }; const setIncomeRow = (id, changes) => setB(prev => ({ ...prev, extraIncome: (prev.extraIncome || []).map(r => r.id === id ? { ...r, ...changes } : r), })); const delIncome = (id) => setB(prev => ({ ...prev, extraIncome: (prev.extraIncome || []).filter(r => r.id !== id) })); // ── Berechnungen ────────────────────────────────────────────── const autoFix = calcFixAuto(); const empCalcRows = employees.map(emp => { const auto = calcEmpAuto(emp, b.agZuschlag); const row = b.empRows.find(r => r.id === emp.id) || { aktiv: true, kostenOverride: false, kostenManual: 0, stundenOverride: false, stundenManual: 0 }; const kosten = row.kostenOverride ? (row.kostenManual || 0) : auto.totalKosten; const stunden = row.stundenOverride ? (row.stundenManual || 0) : auto.jahresStunden; return { emp, auto, row, kosten, stunden, aktiv: row.aktiv }; }); const personalKosten = empCalcRows.filter(r => r.aktiv).reduce((s, r) => s + r.kosten, 0); const jahresStundenTotal = empCalcRows.filter(r => r.aktiv).reduce((s, r) => s + r.stunden, 0); const jahresStundenFallback = employees.length === 0 ? 1800 : jahresStundenTotal; const fixCalcRows = autoFix.map(f => { const row = b.fixRows.find(r => r.id === f.id) || { aktiv: true, amountOverride: false, amountManual: f.amount }; const amount = row.amountOverride ? (row.amountManual || 0) : f.amount; return { ...f, row, amount, aktiv: row.aktiv }; }); const fixKosten = fixCalcRows.filter(r => r.aktiv).reduce((s, r) => s + r.amount, 0); const extraKosten = b.extraRows.filter(r => r.aktiv).reduce((s, r) => s + (r.amount || 0), 0); const basisKosten = personalKosten + fixKosten + extraKosten; const reserve = basisKosten * ((b.reserve || 0) / 100); const gesamtKosten = basisKosten + reserve; const produktivStunden = Math.round(jahresStundenFallback * ((b.produktivQuote || 70) / 100)); const selbstkosten = produktivStunden > 0 ? gesamtKosten / produktivStunden : 0; const zielHonorar = selbstkosten > 0 ? selbstkosten / (1 - (b.zielMarge || 25) / 100) : 0; const defaultRate = data.settings.defaultHourlyRate || 120; const currentRate = b.rateOverride ? (b.rateManual || defaultRate) : defaultRate; // ── Einnahmen ───────────────────────────────────────────────── const yearInvoices = (data.invoices || []).filter(i => (i.date || "").startsWith(String(currentYear))); const paidRevenue = yearInvoices.filter(i => i.status === "bezahlt").reduce((s, i) => s + (i.sub || 0), 0); const sentRevenue = yearInvoices.filter(i => i.status === "gesendet" || i.status === "überfällig").reduce((s, i) => s + (i.sub || 0), 0); const draftRevenue = yearInvoices.filter(i => i.status === "entwurf").reduce((s, i) => s + (i.sub || 0), 0); const invoiceAutoRevenue = b.inkludiereRechnungen ? paidRevenue + sentRevenue + (b.inkludiereEntwuerfe ? draftRevenue : 0) : 0; const yearQuotes = (data.quotes || []).filter(q => (q.date || "").startsWith(String(currentYear))); const acceptedQuoteRevenue = yearQuotes.filter(q => q.status === "angenommen").reduce((s, q) => s + (q.sub || 0), 0); const sentQuoteRevenue = yearQuotes.filter(q => q.status === "gesendet").reduce((s, q) => s + (q.sub || 0), 0); const draftQuoteRevenue = yearQuotes.filter(q => q.status === "entwurf").reduce((s, q) => s + (q.sub || 0), 0); const quoteAutoRevenue = b.inkludiereOfferten ? acceptedQuoteRevenue + sentQuoteRevenue + (b.inkludiereOffertEntwuerfe ? draftQuoteRevenue : 0) : 0; const autoRevenue = invoiceAutoRevenue + quoteAutoRevenue; const manualIncomeTotal = (b.extraIncome || []).filter(r => r.aktiv).reduce((s, r) => s + (r.amount || 0), 0); const totalRevenue = autoRevenue + manualIncomeTotal; const ergebnis = totalRevenue - gesamtKosten; // ── IST-Daten aus Zeiterfassung ─────────────────────────────── const NON_BILLING_CATEGORIES = ["Wettbewerb"]; const projects = data.projects || []; const wbProjIds = new Set(projects.filter(p => NON_BILLING_CATEGORIES.includes(p.category)).map(p => p.id)); const yearEntries = (data.timeEntries || []).filter(e => (e.date || "").startsWith(String(currentYear))); const totalTrackedMins = yearEntries.reduce((s, e) => s + (e.minutes || 0), 0); const billingMins = yearEntries.filter(e => e.projectId && !wbProjIds.has(e.projectId)).reduce((s, e) => s + (e.minutes || 0), 0); const wbTotalMins = yearEntries.filter(e => wbProjIds.has(e.projectId)).reduce((s, e) => s + (e.minutes || 0), 0); const noProjectMins = yearEntries.filter(e => !e.projectId).reduce((s, e) => s + (e.minutes || 0), 0); const istProduktivQuote = totalTrackedMins > 0 ? (billingMins / totalTrackedMins) * 100 : null; const wbProjects = projects.filter(p => NON_BILLING_CATEGORIES.includes(p.category)); const wbRows = wbProjects.map(p => { const mins = yearEntries.filter(e => e.projectId === p.id).reduce((s, e) => s + (e.minutes || 0), 0); return { id: p.id, name: p.name, mins, hours: mins / 60, cost: (mins / 60) * selbstkosten }; }).filter(r => r.mins > 0).sort((a, b) => b.mins - a.mins); const wbTotalHours = wbTotalMins / 60; const rateDiff = currentRate - zielHonorar; const rateOk = rateDiff >= 0; // ── Versionen ──────────────────────────────────────────────── const versions = data.settings.studioBudgetVersions || []; const [showVersions, setShowVersions] = useState(false); const [saveName, setSaveName] = useState(""); const buildSnapshot = (name) => ({ id: generateId(), name: name || `Budget ${new Date().toLocaleDateString("de-CH")}`, savedAt: new Date().toISOString(), b: { agZuschlag: b.agZuschlag, empRows: b.empRows, fixRows: b.fixRows, extraRows: b.extraRows, extraIncome: b.extraIncome, inkludiereRechnungen: b.inkludiereRechnungen, inkludiereEntwuerfe: b.inkludiereEntwuerfe, inkludiereOfferten: b.inkludiereOfferten, inkludiereOffertEntwuerfe: b.inkludiereOffertEntwuerfe, reserve: b.reserve, produktivQuote: b.produktivQuote, zielMarge: b.zielMarge }, results: { personalKosten, fixKosten, extraKosten, basisKosten, reserve: basisKosten * ((b.reserve || 0) / 100), gesamtKosten, totalRevenue, ergebnis, jahresStundenTotal: jahresStundenFallback, produktivStunden, selbstkosten, zielHonorar, currentRate }, empSnapshot: empCalcRows.map(r => ({ name: r.emp.name, kosten: r.kosten, stunden: r.stunden, aktiv: r.aktiv })), fixSnapshot: [...fixCalcRows.filter(r => r.aktiv).map(r => ({ label: r.label, amount: r.amount })), ...b.extraRows.filter(r => r.aktiv).map(r => ({ label: r.label, amount: r.amount }))], }); const saveVersion = () => { const name = saveName.trim() || `Budget ${new Date().toLocaleDateString("de-CH")}`; const snap = buildSnapshot(name); update("settings", { ...data.settings, studioBudgetVersions: [...versions, snap], studioBudget: { agZuschlag: b.agZuschlag, empRows: b.empRows, fixRows: b.fixRows, extraRows: b.extraRows, reserve: b.reserve, produktivQuote: b.produktivQuote, zielMarge: b.zielMarge } }); setSaveName(""); }; const loadVersion = (v) => { setB(prev => ({ ...prev, ...v.b })); setShowVersions(false); }; const deleteVersion = (id) => { update("settings", { ...data.settings, studioBudgetVersions: versions.filter(v => v.id !== id) }); }; const saveSettings = () => { update("settings", { ...data.settings, studioBudget: { agZuschlag: b.agZuschlag, empRows: b.empRows, fixRows: b.fixRows, extraRows: b.extraRows, extraIncome: b.extraIncome, inkludiereRechnungen: b.inkludiereRechnungen, inkludiereEntwuerfe: b.inkludiereEntwuerfe, inkludiereOfferten: b.inkludiereOfferten, inkludiereOffertEntwuerfe: b.inkludiereOffertEntwuerfe, reserve: b.reserve, produktivQuote: b.produktivQuote, zielMarge: b.zielMarge, rateOverride: b.rateOverride, rateManual: b.rateManual, }}); }; // ── Kleines Inline-Override-Feld ───────────────────────────── const OverrideInput = ({ label, aktiv, checked, onToggle, value, onChange, autoVal, unit = "CHF", step = 100 }) => (
{checked && aktiv ? ( onChange(+e.target.value)} style={{ width: 110, height: 28, fontSize: 12, padding: "0 8px" }} /> ) : ( {unit === "CHF" ? formatCHF(autoVal) : `${autoVal}h`} )}
); const SectionLabel = ({ children }) => (
{children}
); return (
} /> {/* Versions-Panel */} {showVersions && (
VERSIONEN
{/* Neue Version speichern */}
setSaveName(e.target.value)} placeholder={`z.B. Budget ${new Date().getFullYear()}, Szenario A…`} style={{ flex: 1, height: 34, fontSize: 12 }} onKeyDown={e => e.key === "Enter" && saveVersion()} />
{versions.length === 0 ? (
Noch keine Versionen gespeichert.
) : ( {[...versions].reverse().map(v => ( ))}
Name Selbstkosten/h Ziel-Honorar/h Gesamtkosten Gespeichert
{v.name} {formatCHF(Math.round(v.results?.selbstkosten || 0))} {formatCHF(Math.round(v.results?.zielHonorar || 0))} {formatCHF(Math.round(v.results?.gesamtKosten || 0))} {new Date(v.savedAt).toLocaleDateString("de-CH")}
)}
)} {/* ── ANALYSE — full width, ganz oben ── */}
SELBSTKOSTENSATZ
{formatCHF(Math.round(selbstkosten))}
pro produktive Stunde
ZIEL-HONORAR
{formatCHF(Math.round(zielHonorar))}
inkl. {b.zielMarge}% Marge
GESAMTKOSTEN / JAHR
{formatCHF(gesamtKosten)}
{jahresStundenFallback}h verfügbar · {produktivStunden}h produktiv
{rateOk ? "✓ Ansatz ausreichend" : "⚠ Ansatz zu tief"}
{rateOk ? "+" : ""}{formatCHF(Math.round(rateDiff))}
Aktuell {formatCHF(currentRate)}/h
{selbstkosten > 0 && (
Break-even: {Math.ceil(gesamtKosten / currentRate)}h
)}
{/* ── LINKE SPALTE ── */}
{/* Personalkosten */}
PERSONALKOSTEN / JAHR
{ const v = +e.target.value; setField("agZuschlag", v); setB(prev => ({ ...prev, agZuschlag: v })); }} />
AHV, UVG, BVG, KTG AG-Anteil (typisch 18–22%)
{employees.length === 0 ? (
Keine Mitarbeiter erfasst – unter «Mitarbeiter» Löhne hinterlegen.
) : ( {empCalcRows.map(({ emp, auto, row, kosten, stunden, aktiv }) => ( ))}
Mitarbeiter Kosten/Jahr Jahresstunden
setEmpRow(emp.id, { kostenOverride: v })} value={row.kostenManual} onChange={v => setEmpRow(emp.id, { kostenManual: v })} autoVal={auto.totalKosten} step={500} /> setEmpRow(emp.id, { stundenOverride: v })} value={row.stundenManual} onChange={v => setEmpRow(emp.id, { stundenManual: v })} autoVal={auto.jahresStunden} unit="h" step={10} />
)}
Total Personalkosten {formatCHF(personalKosten)}
{/* Fixkosten */}
FIXKOSTEN / JAHR {fixCalcRows.length === 0 && b.extraRows.length === 0 ? (
Keine internen Ausgaben erfasst – unter «Interne Ausgaben» hinterlegen oder manuell unten hinzufügen.
) : ( {fixCalcRows.map(f => ( ))} {b.extraRows.map(r => ( ))}
Posten CHF/Jahr
setFixRow(f.id, { amountOverride: v })} value={f.row.amountManual} onChange={v => setFixRow(f.id, { amountManual: v })} autoVal={f.amount} step={100} />
setExtraRow(r.id, { amount: +e.target.value })} style={{ width: 110, height: 28, fontSize: 12, padding: "0 8px" }} />
)} {/* Neuen Posten hinzufügen */}
setNewExtra(p => ({ ...p, label: e.target.value }))} placeholder="Bezeichnung (z.B. Buchführung)" style={{ flex: 1, height: 32, fontSize: 12 }} /> setNewExtra(p => ({ ...p, amount: +e.target.value }))} placeholder="CHF/Jahr" style={{ width: 100, height: 32, fontSize: 12 }} />
Total Fixkosten {formatCHF(fixKosten + extraKosten)}
{/* Einnahmen */}
EINNAHMEN / JAHR
{b.inkludiereRechnungen && (
AUS RECHNUNGEN {currentYear}
{[ { label: "Bezahlt", value: paidRevenue, color: "#2d6a4f" }, { label: "Versendet / Überfällig", value: sentRevenue, color: "#b07848" }, ].map(({ label, value, color }) => value > 0 && (
{label} {formatCHF(value)}
))} {draftRevenue > 0 && (
)} {paidRevenue === 0 && sentRevenue === 0 && draftRevenue === 0 && (
Keine Rechnungen in {currentYear} gefunden.
)}
)} {b.inkludiereOfferten && (
AUS OFFERTEN {currentYear}
{[ { label: "Angenommen", value: acceptedQuoteRevenue, color: "#2d6a4f" }, { label: "Gesendet", value: sentQuoteRevenue, color: "#b07848" }, ].map(({ label, value, color }) => value > 0 && (
{label} {formatCHF(value)}
))} {draftQuoteRevenue > 0 && (
)} {acceptedQuoteRevenue === 0 && sentQuoteRevenue === 0 && draftQuoteRevenue === 0 && (
Keine Offerten in {currentYear} gefunden.
)}
)} {(b.extraIncome || []).length > 0 && ( {(b.extraIncome || []).map(r => ( ))}
setIncomeRow(r.id, { amount: +e.target.value })} style={{ width: 110, height: 28, fontSize: 12, padding: "0 8px" }} />
)}
setNewIncome(p => ({ ...p, label: e.target.value }))} placeholder="z.B. Subvention, Förderung…" style={{ flex: 1, height: 32, fontSize: 12 }} onKeyDown={e => e.key === "Enter" && addIncome()} /> setNewIncome(p => ({ ...p, amount: +e.target.value }))} placeholder="CHF/Jahr" style={{ width: 100, height: 32, fontSize: 12 }} />
Total Einnahmen {formatCHF(totalRevenue)}
{/* Reserve & Stunden & Marge — kompakt in einer Zeile */}
PARAMETER
{[ { label: "Reserve %", key: "reserve", min: 0, max: 50, hint: "Puffer für Unvorhergesehenes" }, { label: "Produktiv-Quote %", key: "produktivQuote", min: 10, max: 100, hint: "Verrechenbare Stunden (60–75%)" }, { label: "Ziel-Marge %", key: "zielMarge", min: 0, max: 80, hint: "Aufschlag für Gewinn / Reinvestition" }, ].map(({ label, key, min, max, hint }) => (
{label}
setField(key, +e.target.value)} style={{ width: "100%", height: 34, fontSize: 13, padding: "0 10px" }} />
{hint}
))}
Aktueller Stundensatz (Vergleichswert)
setField("rateOverride", v)} value={b.rateManual} onChange={v => setField("rateManual", v)} autoVal={defaultRate} unit="CHF" step={5} />
Standard aus Einstellungen: {formatCHF(defaultRate)}/h
{/* ── RECHTE SPALTE: Ergebnis ── */}
{/* Kostenstruktur */}
KOSTENSTRUKTUR / JAHR {[ { label: "Personalkosten", value: personalKosten }, { label: "Fixkosten", value: fixKosten + extraKosten }, { label: "Basiskosten", value: basisKosten, bold: true }, { label: `+ Reserve (${b.reserve}%)`, value: reserve, indent: true }, { label: "Gesamtkosten", value: gesamtKosten, total: true }, ].map((r, i) => (
{r.label} {formatCHF(r.value)}
))} {totalRevenue > 0 && ( <>
Einnahmen {b.inkludiereEntwuerfe && draftRevenue > 0 && b.inkludiereRechnungen && ( inkl. {formatCHF(draftRevenue)} erwartet (Rechnungen) )} {b.inkludiereOffertEntwuerfe && draftQuoteRevenue > 0 && b.inkludiereOfferten && ( inkl. {formatCHF(draftQuoteRevenue)} erwartet (Offerten) )} {formatCHF(totalRevenue)}
Ergebnis = 0 ? "#2d6a4f" : "#8a1a1a" }}> {ergebnis >= 0 ? "+" : ""}{formatCHF(ergebnis)}
)}
{/* Stunden — Soll + Ist zusammen */}
STUNDEN
Verfügbar {jahresStundenFallback}h {employees.length === 0 && Fallback}
Produktiv ({b.produktivQuote}%) {produktivStunden}h
= 80 ? "#8a1a1a" : b.produktivQuote >= 70 ? "#b5621e" : "#2d6a4f", borderRadius: 3 }} />
Über 75% ist anspruchsvoll
{totalTrackedMins > 0 && (
IST {currentYear}
{[ { label: "Verrechenbar", mins: billingMins, color: "#2d6a4f" }, { label: "Wettbewerbe", mins: wbTotalMins, color: "#b5621e" }, { label: "Ohne Projekt", mins: noProjectMins, color: "#bbb" }, ].map(({ label, mins, color }) => { if (mins === 0) return null; const pct = (mins / totalTrackedMins) * 100; return (
{label} {Math.round(mins / 60)}h ({Math.round(pct)}%)
); })}
Total erfasst {Math.round(totalTrackedMins / 60)}h
{istProduktivQuote !== null && (
Ist-Produktivquote 10 ? "#b5621e" : "var(--text)" }}>{Math.round(istProduktivQuote)}% Soll: {b.produktivQuote}%
)}
)}
{/* Wettbewerbe – Interner Aufwand */} {wbProjects.length > 0 && (
WETTBEWERBE — INTERNER AUFWAND {wbRows.length === 0 ? (
Keine Wettbewerb-Stunden in {currentYear} erfasst.
) : ( <> {wbRows.map(r => ( ))}
Projekt Std. Interner Aufwand
{r.name} {Math.round(r.hours * 10) / 10}h {selbstkosten > 0 ? formatCHF(Math.round(r.cost)) : "—"}
Total {Math.round(wbTotalHours * 10) / 10}h {selbstkosten > 0 ? formatCHF(Math.round(wbTotalHours * selbstkosten)) : "—"}
{selbstkosten > 0 && (
Entgangenes Honorar: {Math.round(wbTotalHours)}h × {formatCHF(currentRate)}/h = {formatCHF(Math.round(wbTotalHours * currentRate))}
)} )}
)} {/* Rollenvergleich */} {(data.settings.roles || []).length > 0 && selbstkosten > 0 && (
ROLLEN-VERGLEICH {(data.settings.roles || []).map(role => { const diff = role.rate - zielHonorar; const ok = diff >= 0; return (
{role.label} {formatCHF(role.rate)}/h
{ok ? "+" : ""}{formatCHF(Math.round(diff))} {ok ? "▲" : "▼"}
); })}
Differenz zum Ziel-Honorar von {formatCHF(Math.round(zielHonorar))}/h
)}
); }