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.
) : (
| Name |
Selbstkosten/h |
Ziel-Honorar/h |
Gesamtkosten |
Gespeichert |
|
{[...versions].reverse().map(v => (
| {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
{employees.length === 0 ? (
Keine Mitarbeiter erfasst – unter «Mitarbeiter» Löhne hinterlegen.
) : (
| Mitarbeiter |
Kosten/Jahr |
Jahresstunden |
{empCalcRows.map(({ emp, auto, row, kosten, stunden, aktiv }) => (
|
|
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.
) : (
| Posten |
CHF/Jahr |
{fixCalcRows.map(f => (
|
|
setFixRow(f.id, { amountOverride: v })}
value={f.row.amountManual} onChange={v => setFixRow(f.id, { amountManual: v })} autoVal={f.amount} step={100} />
|
))}
{b.extraRows.map(r => (
|
|
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.
) : (
<>
| Projekt |
Std. |
Interner Aufwand |
{wbRows.map(r => (
| {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
)}
);
}