Install LOC guardrails (check-loc.sh, architecture.md, pre-commit hook) and split all 44 files exceeding 500 LOC into domain-focused modules: - consent-service (Go): models, handlers, services, database splits - backend-core (Python): security_api, rbac_api, pdf_service, auth splits - admin-core (TypeScript): 5 page.tsx + sidebar extractions - pitch-deck (TypeScript): 6 slides, 3 UI components, engine.ts splits - voice-service (Python): enhanced_task_orchestrator split Result: 0 violations, 36 exempted (pipeline, tests, pure-data files). Go build verified clean. No behavior changes — pure structural splits. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
107 lines
3.5 KiB
TypeScript
107 lines
3.5 KiB
TypeScript
/**
|
|
* Sheet Calculators — pure computation functions (no DB dependency)
|
|
*
|
|
* Used by the main engine to compute Personalkosten, Investitionen,
|
|
* and aggregate monthly values.
|
|
*/
|
|
|
|
import {
|
|
MonthlyValues, MONTHS, FOUNDING_MONTH,
|
|
emptyMonthly, dateToMonth, monthToDate,
|
|
FPPersonalkosten, FPInvestitionen,
|
|
} from './types'
|
|
|
|
export function computePersonalkosten(positions: FPPersonalkosten[]): FPPersonalkosten[] {
|
|
return positions.map(p => {
|
|
const brutto = emptyMonthly()
|
|
const sozial = emptyMonthly()
|
|
const total = emptyMonthly()
|
|
|
|
if (!p.start_date || !p.brutto_monthly) return { ...p, values_brutto: brutto, values_sozial: sozial, values_total: total }
|
|
|
|
const startDate = new Date(p.start_date)
|
|
const startM = dateToMonth(startDate.getFullYear(), startDate.getMonth() + 1)
|
|
const endM = p.end_date
|
|
? dateToMonth(new Date(p.end_date).getFullYear(), new Date(p.end_date).getMonth() + 1)
|
|
: MONTHS
|
|
|
|
for (let m = Math.max(1, startM); m <= Math.min(MONTHS, endM); m++) {
|
|
const { year } = monthToDate(m)
|
|
const yearsFromStart = year - startDate.getFullYear()
|
|
const raise = Math.pow(1 + (p.annual_raise_pct || 0) / 100, yearsFromStart)
|
|
const monthlyBrutto = Math.round(p.brutto_monthly * raise)
|
|
|
|
brutto[`m${m}`] = monthlyBrutto
|
|
sozial[`m${m}`] = Math.round(monthlyBrutto * (p.ag_sozial_pct || 20.425) / 100)
|
|
total[`m${m}`] = brutto[`m${m}`] + sozial[`m${m}`]
|
|
}
|
|
|
|
return { ...p, values_brutto: brutto, values_sozial: sozial, values_total: total }
|
|
})
|
|
}
|
|
|
|
export function computeInvestitionen(items: FPInvestitionen[]): FPInvestitionen[] {
|
|
return items.map(item => {
|
|
const invest = emptyMonthly()
|
|
const afa = emptyMonthly()
|
|
|
|
if (!item.purchase_date || !item.purchase_amount) return { ...item, values_invest: invest, values_afa: afa }
|
|
|
|
const d = new Date(item.purchase_date)
|
|
const purchaseM = dateToMonth(d.getFullYear(), d.getMonth() + 1)
|
|
|
|
if (purchaseM >= 1 && purchaseM <= MONTHS) {
|
|
invest[`m${purchaseM}`] = item.purchase_amount
|
|
}
|
|
|
|
// AfA (linear depreciation)
|
|
if (item.afa_years && item.afa_years > 0) {
|
|
const afaMonths = item.afa_years * 12
|
|
const monthlyAfa = Math.round(item.purchase_amount / afaMonths)
|
|
for (let m = purchaseM; m < purchaseM + afaMonths && m <= MONTHS; m++) {
|
|
if (m >= 1) afa[`m${m}`] = monthlyAfa
|
|
}
|
|
} else {
|
|
// GWG: full depreciation in purchase month
|
|
if (purchaseM >= 1 && purchaseM <= MONTHS) {
|
|
afa[`m${purchaseM}`] = item.purchase_amount
|
|
}
|
|
}
|
|
|
|
return { ...item, values_invest: invest, values_afa: afa }
|
|
})
|
|
}
|
|
|
|
export function sumRows(rows: { values: MonthlyValues }[]): MonthlyValues {
|
|
const result = emptyMonthly()
|
|
for (const row of rows) {
|
|
for (let m = 1; m <= MONTHS; m++) {
|
|
result[`m${m}`] += row.values[`m${m}`] || 0
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
export function sumField(rows: { [key: string]: MonthlyValues }[], field: string): MonthlyValues {
|
|
const result = emptyMonthly()
|
|
for (const row of rows) {
|
|
const v = row[field] as MonthlyValues
|
|
if (!v) continue
|
|
for (let m = 1; m <= MONTHS; m++) {
|
|
result[`m${m}`] += v[`m${m}`] || 0
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Compute headcount per month from personal positions.
|
|
*/
|
|
export function computeHeadcount(personal: FPPersonalkosten[]): MonthlyValues {
|
|
const headcount = emptyMonthly()
|
|
for (let m = 1; m <= MONTHS; m++) {
|
|
headcount[`m${m}`] = personal.filter(p => (p.values_total[`m${m}`] || 0) > 0).length
|
|
}
|
|
return headcount
|
|
}
|