feat(pitch-deck): formula engine + tooltips for betriebliche Aufwendungen
All checks were successful
Build pitch-deck / build-push-deploy (push) Successful in 1m27s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 36s
CI / test-python-voice (push) Successful in 36s
CI / test-bqas (push) Successful in 34s
All checks were successful
Build pitch-deck / build-push-deploy (push) Successful in 1m27s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 36s
CI / test-python-voice (push) Successful in 36s
CI / test-bqas (push) Successful in 34s
Engine formulas added: - Berufsgenossenschaft (F): 2.77% of total brutto payroll (VBG IT rate) - Internet/Mobilfunk (F): Headcount × 50 EUR/Mon - Allgemeine Marketingkosten (F): 10% of monthly revenue UI: Hover tooltips on all (F) and computed rows showing the formula. SUMME matcher updated for renamed "SUMME Betriebliche Aufwendungen". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,34 @@ function getLabel(row: SheetRow): string {
|
|||||||
return row.row_label || row.person_name || row.item_name || '—'
|
return row.row_label || row.person_name || row.item_name || '—'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FORMULA_TOOLTIPS: Record<string, string> = {
|
||||||
|
'Fort-/Weiterbildungskosten (F)': 'Mitarbeiter (ohne Gründer) × 500 EUR/Mon',
|
||||||
|
'Fahrzeugkosten (F)': 'Mitarbeiter (ohne Gründer) × 400 EUR/Mon',
|
||||||
|
'KFZ-Steuern (F)': 'Mitarbeiter (ohne Gründer) × 50 EUR/Mon',
|
||||||
|
'KFZ-Versicherung (F)': 'Mitarbeiter (ohne Gründer) × 500 EUR/Mon',
|
||||||
|
'Reisekosten (F)': 'Headcount gesamt × 100 EUR/Mon',
|
||||||
|
'Bewirtungskosten (F)': 'Enterprise-Kunden × 200 EUR/Mon',
|
||||||
|
'Internet/Mobilfunk (F)': 'Headcount gesamt × 50 EUR/Mon',
|
||||||
|
'Serverkosten Cloud (F)': 'Bestandskunden × 100 EUR + 500 EUR Basis',
|
||||||
|
'Berufsgenossenschaft (F)': '2,77% der Brutto-Lohnsumme (VBG IT)',
|
||||||
|
'Allgemeine Marketingkosten (F)': '10% vom Monatsumsatz',
|
||||||
|
'Personalkosten': 'Summe aus Tab Personalkosten',
|
||||||
|
'Abschreibungen': 'Summe AfA aus Tab Investitionen',
|
||||||
|
}
|
||||||
|
|
||||||
|
function LabelWithTooltip({ label }: { label: string }) {
|
||||||
|
const tooltip = FORMULA_TOOLTIPS[label]
|
||||||
|
if (!tooltip) return <span>{label}</span>
|
||||||
|
return (
|
||||||
|
<span className="group relative cursor-help">
|
||||||
|
{label}
|
||||||
|
<span className="invisible group-hover:visible absolute left-0 top-full mt-1 z-50 bg-slate-800 border border-white/10 text-[9px] text-white/70 px-2 py-1 rounded shadow-lg whitespace-nowrap">
|
||||||
|
{tooltip}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function getValues(row: SheetRow): Record<string, number> {
|
function getValues(row: SheetRow): Record<string, number> {
|
||||||
return row.values || row.values_total || row.values_brutto || {}
|
return row.values || row.values_total || row.values_brutto || {}
|
||||||
}
|
}
|
||||||
@@ -394,7 +422,7 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId
|
|||||||
return (
|
return (
|
||||||
<tr key={row.id} className={`border-b border-white/[0.03] ${isSumRow ? 'bg-white/[0.03]' : ''} hover:bg-white/[0.02]`}>
|
<tr key={row.id} className={`border-b border-white/[0.03] ${isSumRow ? 'bg-white/[0.03]' : ''} hover:bg-white/[0.02]`}>
|
||||||
<td className={`py-1.5 px-2 sticky left-0 bg-slate-900/90 backdrop-blur ${isSumRow ? 'font-bold text-white/80' : 'text-white/60'}`}>
|
<td className={`py-1.5 px-2 sticky left-0 bg-slate-900/90 backdrop-blur ${isSumRow ? 'font-bold text-white/80' : 'text-white/60'}`}>
|
||||||
{label}
|
<LabelWithTooltip label={label} />
|
||||||
</td>
|
</td>
|
||||||
{[2026, 2027, 2028, 2029, 2030].map(y => {
|
{[2026, 2027, 2028, 2029, 2030].map(y => {
|
||||||
const v = values[`y${y}`] || 0
|
const v = values[`y${y}`] || 0
|
||||||
@@ -453,7 +481,7 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId
|
|||||||
<td className={`py-1 px-2 sticky left-0 bg-slate-900/90 backdrop-blur ${isSumRow ? 'font-bold text-white/80' : 'text-white/60'}`}>
|
<td className={`py-1 px-2 sticky left-0 bg-slate-900/90 backdrop-blur ${isSumRow ? 'font-bold text-white/80' : 'text-white/60'}`}>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{isEditable && <span className="w-1 h-1 rounded-full bg-indigo-400 flex-shrink-0" />}
|
{isEditable && <span className="w-1 h-1 rounded-full bg-indigo-400 flex-shrink-0" />}
|
||||||
<span className="truncate">{label}</span>
|
<span className="truncate"><LabelWithTooltip label={label} /></span>
|
||||||
{row.position && <span className="text-white/50 ml-1">({row.position})</span>}
|
{row.position && <span className="text-white/50 ml-1">({row.position})</span>}
|
||||||
{row.section && <span className="text-white/50 ml-1">[{row.section}]</span>}
|
{row.section && <span className="text-white/50 ml-1">[{row.section}]</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -237,6 +237,7 @@ export async function computeFinanzplan(pool: Pool, scenarioId: string): Promise
|
|||||||
{ label: 'KFZ-Versicherung (F)', perUnit: 500, source: hcWithoutFounders },
|
{ label: 'KFZ-Versicherung (F)', perUnit: 500, source: hcWithoutFounders },
|
||||||
{ label: 'Reisekosten (F)', perUnit: 100, source: headcount },
|
{ label: 'Reisekosten (F)', perUnit: 100, source: headcount },
|
||||||
{ label: 'Bewirtungskosten (F)', perUnit: 200, source: enterpriseKunden },
|
{ label: 'Bewirtungskosten (F)', perUnit: 200, source: enterpriseKunden },
|
||||||
|
{ label: 'Internet/Mobilfunk (F)', perUnit: 50, source: headcount },
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const fr of formulaRows) {
|
for (const fr of formulaRows) {
|
||||||
@@ -251,6 +252,28 @@ export async function computeFinanzplan(pool: Pool, scenarioId: string): Promise
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Berufsgenossenschaft: 2.77% of total brutto payroll
|
||||||
|
const bgRow = betrieb.find(r => r.row_label.includes('Berufsgenossenschaft'))
|
||||||
|
if (bgRow) {
|
||||||
|
const computed = emptyMonthly()
|
||||||
|
for (let m = 1; m <= MONTHS; m++) {
|
||||||
|
computed[`m${m}`] = Math.round((totalBrutto[`m${m}`] || 0) * 0.0277)
|
||||||
|
}
|
||||||
|
await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(computed), bgRow.id])
|
||||||
|
bgRow.values = computed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allgemeine Marketingkosten: 10% of revenue
|
||||||
|
const marketingRow = betrieb.find(r => r.row_label.includes('Allgemeine Marketingkosten'))
|
||||||
|
if (marketingRow) {
|
||||||
|
const computed = emptyMonthly()
|
||||||
|
for (let m = 1; m <= MONTHS; m++) {
|
||||||
|
computed[`m${m}`] = Math.round((totalRevenue[`m${m}`] || 0) * 0.10)
|
||||||
|
}
|
||||||
|
await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(computed), marketingRow.id])
|
||||||
|
marketingRow.values = computed
|
||||||
|
}
|
||||||
|
|
||||||
// Serverkosten: Bestandskunden * 100 + 500 Basis
|
// Serverkosten: Bestandskunden * 100 + 500 Basis
|
||||||
const totalKunden = emptyMonthly()
|
const totalKunden = emptyMonthly()
|
||||||
for (const row of kundenRows.rows) {
|
for (const row of kundenRows.rows) {
|
||||||
|
|||||||
Reference in New Issue
Block a user