@jjlmoya/utils-finance 1.29.0 → 1.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/category/index.ts +2 -1
- package/src/entries.ts +4 -1
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/salaryEmployerCost/bibliography.astro +6 -0
- package/src/tool/salaryEmployerCost/bibliography.ts +16 -0
- package/src/tool/salaryEmployerCost/component.astro +130 -0
- package/src/tool/salaryEmployerCost/controller.ts +129 -0
- package/src/tool/salaryEmployerCost/dom-views.ts +87 -0
- package/src/tool/salaryEmployerCost/entry.ts +31 -0
- package/src/tool/salaryEmployerCost/evaluator.ts +9 -0
- package/src/tool/salaryEmployerCost/i18n/de.ts +159 -0
- package/src/tool/salaryEmployerCost/i18n/en.ts +148 -0
- package/src/tool/salaryEmployerCost/i18n/es.ts +148 -0
- package/src/tool/salaryEmployerCost/i18n/fr.ts +159 -0
- package/src/tool/salaryEmployerCost/i18n/id.ts +159 -0
- package/src/tool/salaryEmployerCost/i18n/it.ts +159 -0
- package/src/tool/salaryEmployerCost/i18n/ja.ts +159 -0
- package/src/tool/salaryEmployerCost/i18n/ko.ts +159 -0
- package/src/tool/salaryEmployerCost/i18n/nl.ts +159 -0
- package/src/tool/salaryEmployerCost/i18n/pl.ts +159 -0
- package/src/tool/salaryEmployerCost/i18n/pt.ts +159 -0
- package/src/tool/salaryEmployerCost/i18n/ru.ts +159 -0
- package/src/tool/salaryEmployerCost/i18n/sv.ts +159 -0
- package/src/tool/salaryEmployerCost/i18n/tr.ts +159 -0
- package/src/tool/salaryEmployerCost/i18n/zh.ts +159 -0
- package/src/tool/salaryEmployerCost/index.ts +11 -0
- package/src/tool/salaryEmployerCost/logic.test.ts +88 -0
- package/src/tool/salaryEmployerCost/logic.ts +133 -0
- package/src/tool/salaryEmployerCost/seo.astro +15 -0
- package/src/tool/salaryEmployerCost/spain-gross-net-salary-employer-cost-calculator.css +522 -0
- package/src/tool/salaryEmployerCost/storage.ts +35 -0
- package/src/tool/salaryEmployerCost/ui.ts +41 -0
- package/src/tools.ts +2 -2
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -15,10 +15,11 @@ import { fireCalculator } from '../tool/fireCalculator/entry';
|
|
|
15
15
|
import { debtSnowball } from '../tool/debtSnowball/entry';
|
|
16
16
|
import { revolvingCardCalculator } from '../tool/revolvingCardCalculator/entry';
|
|
17
17
|
import { pesetaValue } from '../tool/pesetaValue/entry';
|
|
18
|
+
import { salaryEmployerCost } from '../tool/salaryEmployerCost/entry';
|
|
18
19
|
|
|
19
20
|
export const financeCategory: FinanceCategoryEntry = {
|
|
20
21
|
icon: 'mdi:finance',
|
|
21
|
-
tools: [compoundInterest, financialFreedom, profitability, mortgage, inflation, percentageCalculator, lateInterest, ibanBicSwift, rentIncrease, lotteryOptimizer, courtFeeCalculator, legalInterestRate, fireCalculator, debtSnowball, revolvingCardCalculator, pesetaValue],
|
|
22
|
+
tools: [compoundInterest, financialFreedom, profitability, mortgage, inflation, percentageCalculator, lateInterest, ibanBicSwift, rentIncrease, lotteryOptimizer, courtFeeCalculator, legalInterestRate, fireCalculator, debtSnowball, revolvingCardCalculator, pesetaValue, salaryEmployerCost],
|
|
22
23
|
i18n: {
|
|
23
24
|
en: () => import('./i18n/en').then((m) => m.content),
|
|
24
25
|
es: () => import('./i18n/es').then((m) => m.content),
|
package/src/entries.ts
CHANGED
|
@@ -30,6 +30,8 @@ export { revolvingCardCalculator } from './tool/revolvingCardCalculator/entry';
|
|
|
30
30
|
export type { RevolvingCardCalculatorLocaleContent } from './tool/revolvingCardCalculator/entry';
|
|
31
31
|
export { pesetaValue } from './tool/pesetaValue/entry';
|
|
32
32
|
export type { PesetaValueLocaleContent } from './tool/pesetaValue/entry';
|
|
33
|
+
export { salaryEmployerCost } from './tool/salaryEmployerCost/entry';
|
|
34
|
+
export type { SalaryEmployerCostLocaleContent } from './tool/salaryEmployerCost/entry';
|
|
33
35
|
export { financeCategory } from './category';
|
|
34
36
|
import { compoundInterest } from './tool/compoundInterest/entry';
|
|
35
37
|
import { financialFreedom } from './tool/financialFreedom/entry';
|
|
@@ -47,4 +49,5 @@ import { percentageCalculator } from './tool/percentageCalculator/entry';
|
|
|
47
49
|
import { rentIncrease } from './tool/rentIncreaseCalculator/entry';
|
|
48
50
|
import { revolvingCardCalculator } from './tool/revolvingCardCalculator/entry';
|
|
49
51
|
import { pesetaValue } from './tool/pesetaValue/entry';
|
|
50
|
-
|
|
52
|
+
import { salaryEmployerCost } from './tool/salaryEmployerCost/entry';
|
|
53
|
+
export const ALL_ENTRIES = [compoundInterest, financialFreedom, profitability, courtFeeCalculator, debtSnowball, fireCalculator, ibanBicSwift, inflation, lateInterest, legalInterestRate, lotteryOptimizer, mortgage, percentageCalculator, rentIncrease, revolvingCardCalculator, pesetaValue, salaryEmployerCost];
|
package/src/index.ts
CHANGED
|
@@ -31,3 +31,4 @@ export { FIRE_CALCULATOR_TOOL } from './tool/fireCalculator';
|
|
|
31
31
|
export { DEBT_SNOWBALL_TOOL } from './tool/debtSnowball';
|
|
32
32
|
export { REVOLVING_CARD_CALCULATOR_TOOL } from './tool/revolvingCardCalculator';
|
|
33
33
|
export { PESETA_VALUE_TOOL } from './tool/pesetaValue';
|
|
34
|
+
export { SALARY_EMPLOYER_COST_TOOL } from './tool/salaryEmployerCost';
|
|
@@ -4,8 +4,8 @@ import { financeCategory } from '../data';
|
|
|
4
4
|
|
|
5
5
|
describe('Tool Validation Suite', () => {
|
|
6
6
|
describe('Library Registration', () => {
|
|
7
|
-
it('should have
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
7
|
+
it('should have 17 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(17);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('financeCategory should be defined', () => {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'Spanish Social Security contribution bases and rates for 2026',
|
|
6
|
+
url: 'https://www.boe.es/eli/es/o/2026/03/30/pjc297',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'Spanish Tax Agency personal income tax withholding algorithm for 2026',
|
|
10
|
+
url: 'https://sede.agenciatributaria.gob.es/static_files/Sede/Programas_ayuda/Retenciones/2026/ALGORITMO_2026.pdf',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'Spain 2026 occupational accident and disease premium table',
|
|
14
|
+
url: 'https://www.boe.es/buscar/act.php?id=BOE-A-2026-2548&p=20260204&tn=1',
|
|
15
|
+
},
|
|
16
|
+
];
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { calculateSalaryCost, DEFAULT_SALARY_INPUT } from './logic';
|
|
3
|
+
import type { SalaryEmployerCostUI } from './ui';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
ui: SalaryEmployerCostUI;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { ui } = Astro.props;
|
|
10
|
+
const initial = calculateSalaryCost(DEFAULT_SALARY_INPUT);
|
|
11
|
+
if (!initial) return null;
|
|
12
|
+
const money = (value: number) => new Intl.NumberFormat(ui.currencyLocale, {
|
|
13
|
+
style: 'currency',
|
|
14
|
+
currency: 'EUR',
|
|
15
|
+
maximumFractionDigits: 2,
|
|
16
|
+
}).format(value);
|
|
17
|
+
const netShare = initial.annualNet / initial.annualEmployerCost * 100;
|
|
18
|
+
const irpfShare = netShare + initial.annualIrpf / initial.annualEmployerCost * 100;
|
|
19
|
+
const employeeShare = irpfShare + initial.annualEmployeeSocialSecurity / initial.annualEmployerCost * 100;
|
|
20
|
+
const orbitStyle = `--orbit-net:${netShare}%;--orbit-irpf:${irpfShare}%;--orbit-employee:${employeeShare}%`;
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
<section class="salary-orbit" data-salary-tool data-valid="true" style={orbitStyle}>
|
|
24
|
+
<form class="salary-orbit-console" novalidate>
|
|
25
|
+
<div class="salary-orbit-eyebrow">{ui.labelGrossSalary}</div>
|
|
26
|
+
<div class="salary-orbit-amount">
|
|
27
|
+
<label class="salary-orbit-amount-field" for="salary-orbit-amount">
|
|
28
|
+
<span>{ui.labelGrossSalaryHint}</span>
|
|
29
|
+
<span class="salary-orbit-number-wrap">
|
|
30
|
+
<input id="salary-orbit-amount" data-input-amount type="number" min="17094" max="240000" step="100" value="30000" inputmode="decimal" />
|
|
31
|
+
<b aria-hidden="true">€</b>
|
|
32
|
+
</span>
|
|
33
|
+
</label>
|
|
34
|
+
<div class="salary-orbit-switch" role="group" aria-label={ui.labelGrossSalary}>
|
|
35
|
+
<button type="button" class="is-active" data-field="amountMode" data-value="annual" aria-pressed="true">{ui.labelAnnual}</button>
|
|
36
|
+
<button type="button" data-field="amountMode" data-value="monthly" aria-pressed="false">{ui.labelMonthly}</button>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
<input class="salary-orbit-beam" data-input-amount type="range" min="17094" max="240000" step="100" value="30000" aria-label={ui.labelGrossSalary} />
|
|
40
|
+
|
|
41
|
+
<div class="salary-orbit-choice-line">
|
|
42
|
+
<fieldset>
|
|
43
|
+
<legend>{ui.labelPaySchedule}</legend>
|
|
44
|
+
<div class="salary-orbit-switch">
|
|
45
|
+
<button type="button" data-field="payPeriods" data-value="12" aria-pressed="false">{ui.labelTwelvePayments}</button>
|
|
46
|
+
<button type="button" class="is-active" data-field="payPeriods" data-value="14" aria-pressed="true">{ui.labelFourteenPayments}</button>
|
|
47
|
+
</div>
|
|
48
|
+
</fieldset>
|
|
49
|
+
<fieldset>
|
|
50
|
+
<legend>{ui.labelContract}</legend>
|
|
51
|
+
<div class="salary-orbit-switch">
|
|
52
|
+
<button type="button" class="is-active" data-field="contractType" data-value="permanent" aria-pressed="true">{ui.labelPermanent}</button>
|
|
53
|
+
<button type="button" data-field="contractType" data-value="temporary" aria-pressed="false">{ui.labelTemporary}</button>
|
|
54
|
+
</div>
|
|
55
|
+
</fieldset>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<label class="salary-orbit-dial" for="salary-orbit-irpf">
|
|
59
|
+
<span><b>{ui.labelIrpfRate}</b><small>{ui.labelIrpfHint}</small></span>
|
|
60
|
+
<output data-output-irpf for="salary-orbit-irpf">15.0%</output>
|
|
61
|
+
<input id="salary-orbit-irpf" data-input-irpf type="range" min="0" max="47" step="0.1" value="15" />
|
|
62
|
+
</label>
|
|
63
|
+
|
|
64
|
+
<label class="salary-orbit-dial" for="salary-orbit-risk">
|
|
65
|
+
<span><b>{ui.labelRiskRate}</b><small>{ui.labelRiskHint}</small></span>
|
|
66
|
+
<output data-output-risk for="salary-orbit-risk">1.50%</output>
|
|
67
|
+
<input id="salary-orbit-risk" data-input-risk type="range" min="0.9" max="7.15" step="0.05" value="1.5" />
|
|
68
|
+
</label>
|
|
69
|
+
<div class="salary-orbit-presets" role="group" aria-label={ui.labelRiskRate}>
|
|
70
|
+
<button type="button" class="is-active" data-field="riskRate" data-value="1.5" aria-pressed="true">{ui.labelOfficePreset}</button>
|
|
71
|
+
<button type="button" data-field="riskRate" data-value="2" aria-pressed="false">{ui.labelTradePreset}</button>
|
|
72
|
+
<button type="button" data-field="riskRate" data-value="6.7" aria-pressed="false">{ui.labelConstructionPreset}</button>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div class="salary-orbit-console-foot">
|
|
76
|
+
<span>{ui.labelPrivacy}</span>
|
|
77
|
+
<button type="button" data-action="reset">{ui.labelReset}</button>
|
|
78
|
+
</div>
|
|
79
|
+
</form>
|
|
80
|
+
|
|
81
|
+
<div class="salary-orbit-observatory" aria-live="polite">
|
|
82
|
+
<div class="salary-orbit-view-switch" role="group" aria-label={ui.labelAnnualView}>
|
|
83
|
+
<button type="button" class="is-active" data-field="view" data-value="annual" aria-pressed="true">{ui.labelAnnualView}</button>
|
|
84
|
+
<button type="button" data-field="view" data-value="payment" aria-pressed="false">{ui.labelPerPaymentView}</button>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div class="salary-orbit-status" data-result-state data-state="typical">{ui.labelTypicalGap}</div>
|
|
88
|
+
<div class="salary-orbit-art">
|
|
89
|
+
<div class="salary-orbit-trail salary-orbit-trail-outer"></div>
|
|
90
|
+
<div class="salary-orbit-trail salary-orbit-trail-inner"></div>
|
|
91
|
+
<div class="salary-orbit-disc">
|
|
92
|
+
<span>{ui.labelNetCore}</span>
|
|
93
|
+
<strong data-result-net>{money(initial.annualNet)}</strong>
|
|
94
|
+
<small>{ui.labelWorkerKeeps} <b data-result-keeps>{initial.workerKeepsPercent.toFixed(1)}%</b></small>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="salary-orbit-annotation salary-orbit-annotation-gross">
|
|
97
|
+
<span>{ui.labelGross}</span><strong data-result-gross>{money(initial.annualGross)}</strong>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="salary-orbit-annotation salary-orbit-annotation-irpf">
|
|
100
|
+
<span>{ui.labelIrpf}</span><strong data-result-irpf>{money(initial.annualIrpf)}</strong>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="salary-orbit-annotation salary-orbit-annotation-employee">
|
|
103
|
+
<span>{ui.labelEmployeeSocialSecurity}</span><strong data-result-employee>{money(initial.annualEmployeeSocialSecurity)}</strong>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="salary-orbit-annotation salary-orbit-annotation-employer">
|
|
106
|
+
<span>{ui.labelEmployerHalo}</span><strong data-result-employer>{money(initial.annualEmployerSocialSecurity)}</strong>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div class="salary-orbit-total">
|
|
111
|
+
<span>{ui.labelEmployerCost}</span>
|
|
112
|
+
<strong data-result-cost>{money(initial.annualEmployerCost)}</strong>
|
|
113
|
+
</div>
|
|
114
|
+
<div class="salary-orbit-metrics">
|
|
115
|
+
<div><span>{ui.labelContributionBase}</span><strong data-result-base>{money(initial.monthlyContributionBase)}</strong></div>
|
|
116
|
+
<div><span>{ui.labelCostMultiplier}</span><strong data-result-multiplier>{initial.costMultiplier.toFixed(3)}x</strong></div>
|
|
117
|
+
<div><span>{ui.labelEmployeeDeductions}</span><strong>{ui.labelDeductionsFormula}</strong></div>
|
|
118
|
+
</div>
|
|
119
|
+
<p class="salary-orbit-scope">{ui.labelModelScope} {ui.labelMinimumSalary}</p>
|
|
120
|
+
<div class="salary-orbit-invalid" data-invalid-message>{ui.labelInvalid}</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<script is:inline type="application/json" data-salary-config set:html={JSON.stringify({ ui })} />
|
|
124
|
+
</section>
|
|
125
|
+
|
|
126
|
+
<script>
|
|
127
|
+
import { initSalaryEmployerCost } from './controller';
|
|
128
|
+
|
|
129
|
+
document.querySelectorAll<HTMLElement>('[data-salary-tool]').forEach(initSalaryEmployerCost);
|
|
130
|
+
</script>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_SALARY_INPUT,
|
|
3
|
+
MINIMUM_ANNUAL_GROSS,
|
|
4
|
+
annualizeSalary,
|
|
5
|
+
calculateSalaryCost,
|
|
6
|
+
type ContractType,
|
|
7
|
+
type PayPeriods,
|
|
8
|
+
type SalaryAmountMode,
|
|
9
|
+
type SalaryCostInput,
|
|
10
|
+
} from './logic';
|
|
11
|
+
import { renderInvalidSalary, renderSalaryResult, type SalaryViewMode } from './dom-views';
|
|
12
|
+
import { loadSalaryInput, saveSalaryInput } from './storage';
|
|
13
|
+
import type { SalaryEmployerCostUI } from './ui';
|
|
14
|
+
|
|
15
|
+
interface ToolConfig {
|
|
16
|
+
ui: SalaryEmployerCostUI;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const select = <T extends Element>(root: HTMLElement, selector: string): T | null =>
|
|
20
|
+
root.querySelector<T>(selector);
|
|
21
|
+
|
|
22
|
+
function readConfig(root: HTMLElement): ToolConfig | null {
|
|
23
|
+
const script = select<HTMLScriptElement>(root, '[data-salary-config]');
|
|
24
|
+
if (!script?.textContent) return null;
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(script.textContent) as ToolConfig;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function setPressed(root: HTMLElement, field: string, value: string | number): void {
|
|
33
|
+
root.querySelectorAll<HTMLButtonElement>(`[data-field="${field}"]`).forEach((button) => {
|
|
34
|
+
const active = button.dataset.value === String(value);
|
|
35
|
+
button.classList.toggle('is-active', active);
|
|
36
|
+
button.setAttribute('aria-pressed', String(active));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function setAmountBounds(root: HTMLElement, input: SalaryCostInput): void {
|
|
41
|
+
const minimum = input.amountMode === 'annual'
|
|
42
|
+
? MINIMUM_ANNUAL_GROSS
|
|
43
|
+
: MINIMUM_ANNUAL_GROSS / input.payPeriods;
|
|
44
|
+
const maximum = input.amountMode === 'annual' ? 240_000 : 240_000 / input.payPeriods;
|
|
45
|
+
root.querySelectorAll<HTMLInputElement>('[data-input-amount]').forEach((element) => {
|
|
46
|
+
element.min = String(Math.ceil(minimum));
|
|
47
|
+
element.max = String(Math.floor(maximum));
|
|
48
|
+
const rangeStep = input.amountMode === 'annual' ? '1' : '0.01';
|
|
49
|
+
const numberStep = input.amountMode === 'annual' ? '100' : '10';
|
|
50
|
+
element.step = element.type === 'range' ? rangeStep : numberStep;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function syncControls(root: HTMLElement, input: SalaryCostInput, view: SalaryViewMode): void {
|
|
55
|
+
setAmountBounds(root, input);
|
|
56
|
+
root.querySelectorAll<HTMLInputElement>('[data-input-amount]').forEach((element) => {
|
|
57
|
+
element.value = String(Math.round(input.amount * 100) / 100);
|
|
58
|
+
});
|
|
59
|
+
const irpf = select<HTMLInputElement>(root, '[data-input-irpf]');
|
|
60
|
+
const risk = select<HTMLInputElement>(root, '[data-input-risk]');
|
|
61
|
+
if (irpf) irpf.value = String(input.irpfRate);
|
|
62
|
+
if (risk) risk.value = String(input.riskRate);
|
|
63
|
+
const irpfOutput = select<HTMLOutputElement>(root, '[data-output-irpf]');
|
|
64
|
+
const riskOutput = select<HTMLOutputElement>(root, '[data-output-risk]');
|
|
65
|
+
if (irpfOutput) irpfOutput.value = `${input.irpfRate.toFixed(1)}%`;
|
|
66
|
+
if (riskOutput) riskOutput.value = `${input.riskRate.toFixed(2)}%`;
|
|
67
|
+
setPressed(root, 'amountMode', input.amountMode);
|
|
68
|
+
setPressed(root, 'payPeriods', input.payPeriods);
|
|
69
|
+
setPressed(root, 'contractType', input.contractType);
|
|
70
|
+
setPressed(root, 'view', view);
|
|
71
|
+
setPressed(root, 'riskRate', input.riskRate);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function render(root: HTMLElement, config: ToolConfig, input: SalaryCostInput, view: SalaryViewMode): void {
|
|
75
|
+
syncControls(root, input, view);
|
|
76
|
+
const result = calculateSalaryCost(input);
|
|
77
|
+
if (!result) {
|
|
78
|
+
renderInvalidSalary(root, config.ui);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
renderSalaryResult(root, result, config.ui, view);
|
|
82
|
+
saveSalaryInput(input);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function changeMode(input: SalaryCostInput, mode: SalaryAmountMode): SalaryCostInput {
|
|
86
|
+
const annualGross = annualizeSalary(input);
|
|
87
|
+
const amount = mode === 'annual' ? annualGross : annualGross / input.payPeriods;
|
|
88
|
+
return { ...input, amountMode: mode, amount };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function changePayments(input: SalaryCostInput, payPeriods: PayPeriods): SalaryCostInput {
|
|
92
|
+
const annualGross = annualizeSalary(input);
|
|
93
|
+
const amount = input.amountMode === 'monthly' ? annualGross / payPeriods : input.amount;
|
|
94
|
+
return { ...input, payPeriods, amount };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function changeField(input: SalaryCostInput, field: string, value: string): SalaryCostInput {
|
|
98
|
+
if (field === 'amountMode') return changeMode(input, value as SalaryAmountMode);
|
|
99
|
+
if (field === 'payPeriods') return changePayments(input, Number(value) as PayPeriods);
|
|
100
|
+
if (field === 'contractType') return { ...input, contractType: value as ContractType };
|
|
101
|
+
if (field === 'riskRate') return { ...input, riskRate: Number(value) };
|
|
102
|
+
return input;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function initSalaryEmployerCost(root: HTMLElement): void {
|
|
106
|
+
const config = readConfig(root);
|
|
107
|
+
if (!config) return;
|
|
108
|
+
let input = loadSalaryInput();
|
|
109
|
+
let view: SalaryViewMode = 'annual';
|
|
110
|
+
select<HTMLFormElement>(root, 'form')?.addEventListener('submit', (event) => event.preventDefault());
|
|
111
|
+
render(root, config, input, view);
|
|
112
|
+
root.addEventListener('input', (event) => {
|
|
113
|
+
const target = event.target as HTMLInputElement;
|
|
114
|
+
if (target.matches('[data-input-amount]')) input = { ...input, amount: Number(target.value) };
|
|
115
|
+
if (target.matches('[data-input-irpf]')) input = { ...input, irpfRate: Number(target.value) };
|
|
116
|
+
if (target.matches('[data-input-risk]')) input = { ...input, riskRate: Number(target.value) };
|
|
117
|
+
render(root, config, input, view);
|
|
118
|
+
});
|
|
119
|
+
root.addEventListener('click', (event) => {
|
|
120
|
+
const button = (event.target as Element).closest<HTMLButtonElement>('button');
|
|
121
|
+
if (!button) return;
|
|
122
|
+
if (button.dataset.action === 'reset') input = { ...DEFAULT_SALARY_INPUT };
|
|
123
|
+
if (button.dataset.field === 'view') view = button.dataset.value as SalaryViewMode;
|
|
124
|
+
if (button.dataset.field && button.dataset.value) {
|
|
125
|
+
input = changeField(input, button.dataset.field, button.dataset.value);
|
|
126
|
+
}
|
|
127
|
+
render(root, config, input, view);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { evaluateCostGap } from './evaluator';
|
|
2
|
+
import type { SalaryCostResult } from './logic';
|
|
3
|
+
import type { SalaryEmployerCostUI } from './ui';
|
|
4
|
+
|
|
5
|
+
export type SalaryViewMode = 'annual' | 'payment';
|
|
6
|
+
|
|
7
|
+
const select = <T extends Element>(root: HTMLElement, selector: string): T | null =>
|
|
8
|
+
root.querySelector<T>(selector);
|
|
9
|
+
|
|
10
|
+
function setText(root: HTMLElement, selector: string, value: string): void {
|
|
11
|
+
const element = select<HTMLElement>(root, selector);
|
|
12
|
+
if (element) element.textContent = value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function formatCurrency(value: number, ui: SalaryEmployerCostUI): string {
|
|
16
|
+
return new Intl.NumberFormat(ui.currencyLocale, {
|
|
17
|
+
style: 'currency',
|
|
18
|
+
currency: 'EUR',
|
|
19
|
+
maximumFractionDigits: 2,
|
|
20
|
+
}).format(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function displayValues(result: SalaryCostResult, mode: SalaryViewMode) {
|
|
24
|
+
if (mode === 'annual') {
|
|
25
|
+
return {
|
|
26
|
+
gross: result.annualGross,
|
|
27
|
+
net: result.annualNet,
|
|
28
|
+
irpf: result.annualIrpf,
|
|
29
|
+
employee: result.annualEmployeeSocialSecurity,
|
|
30
|
+
employer: result.annualEmployerSocialSecurity,
|
|
31
|
+
cost: result.annualEmployerCost,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const payments = result.annualGross / result.grossPerPayment;
|
|
35
|
+
return {
|
|
36
|
+
gross: result.grossPerPayment,
|
|
37
|
+
net: result.netPerPayment,
|
|
38
|
+
irpf: result.annualIrpf / payments,
|
|
39
|
+
employee: result.annualEmployeeSocialSecurity / payments,
|
|
40
|
+
employer: result.annualEmployerSocialSecurity / payments,
|
|
41
|
+
cost: result.employerCostPerPayment,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function renderOrbit(root: HTMLElement, result: SalaryCostResult): void {
|
|
46
|
+
const total = result.annualEmployerCost;
|
|
47
|
+
const net = result.annualNet / total * 100;
|
|
48
|
+
const irpf = net + result.annualIrpf / total * 100;
|
|
49
|
+
const employee = irpf + result.annualEmployeeSocialSecurity / total * 100;
|
|
50
|
+
root.style.setProperty('--orbit-net', `${net}%`);
|
|
51
|
+
root.style.setProperty('--orbit-irpf', `${irpf}%`);
|
|
52
|
+
root.style.setProperty('--orbit-employee', `${employee}%`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function renderMetrics(root: HTMLElement, result: SalaryCostResult, ui: SalaryEmployerCostUI): void {
|
|
56
|
+
setText(root, '[data-result-base]', formatCurrency(result.monthlyContributionBase, ui));
|
|
57
|
+
setText(root, '[data-result-multiplier]', `${result.costMultiplier.toFixed(3)}x`);
|
|
58
|
+
setText(root, '[data-result-keeps]', `${result.workerKeepsPercent.toFixed(1)}%`);
|
|
59
|
+
const state = evaluateCostGap(result);
|
|
60
|
+
const labels = { lean: ui.labelLeanGap, typical: ui.labelTypicalGap, wide: ui.labelWideGap };
|
|
61
|
+
setText(root, '[data-result-state]', labels[state]);
|
|
62
|
+
const badge = select<HTMLElement>(root, '[data-result-state]');
|
|
63
|
+
if (badge) badge.dataset.state = state;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function renderSalaryResult(
|
|
67
|
+
root: HTMLElement,
|
|
68
|
+
result: SalaryCostResult,
|
|
69
|
+
ui: SalaryEmployerCostUI,
|
|
70
|
+
mode: SalaryViewMode,
|
|
71
|
+
): void {
|
|
72
|
+
const values = displayValues(result, mode);
|
|
73
|
+
setText(root, '[data-result-gross]', formatCurrency(values.gross, ui));
|
|
74
|
+
setText(root, '[data-result-net]', formatCurrency(values.net, ui));
|
|
75
|
+
setText(root, '[data-result-irpf]', formatCurrency(values.irpf, ui));
|
|
76
|
+
setText(root, '[data-result-employee]', formatCurrency(values.employee, ui));
|
|
77
|
+
setText(root, '[data-result-employer]', formatCurrency(values.employer, ui));
|
|
78
|
+
setText(root, '[data-result-cost]', formatCurrency(values.cost, ui));
|
|
79
|
+
renderMetrics(root, result, ui);
|
|
80
|
+
renderOrbit(root, result);
|
|
81
|
+
root.dataset.valid = 'true';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function renderInvalidSalary(root: HTMLElement, ui: SalaryEmployerCostUI): void {
|
|
85
|
+
root.dataset.valid = 'false';
|
|
86
|
+
setText(root, '[data-invalid-message]', ui.labelInvalid);
|
|
87
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { FinanceToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
import type { SalaryEmployerCostUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type SalaryEmployerCostLocaleContent = ToolLocaleContent<SalaryEmployerCostUI>;
|
|
5
|
+
|
|
6
|
+
export const salaryEmployerCost: FinanceToolEntry<SalaryEmployerCostUI> = {
|
|
7
|
+
id: 'calculadora-salario-bruto-neto-coste-empresa-espana',
|
|
8
|
+
icons: {
|
|
9
|
+
bg: 'mdi:account-cash',
|
|
10
|
+
fg: 'mdi:office-building-outline',
|
|
11
|
+
},
|
|
12
|
+
i18n: {
|
|
13
|
+
de: () => import('./i18n/de').then((module) => module.content),
|
|
14
|
+
en: () => import('./i18n/en').then((module) => module.content),
|
|
15
|
+
es: () => import('./i18n/es').then((module) => module.content),
|
|
16
|
+
fr: () => import('./i18n/fr').then((module) => module.content),
|
|
17
|
+
id: () => import('./i18n/id').then((module) => module.content),
|
|
18
|
+
it: () => import('./i18n/it').then((module) => module.content),
|
|
19
|
+
ja: () => import('./i18n/ja').then((module) => module.content),
|
|
20
|
+
ko: () => import('./i18n/ko').then((module) => module.content),
|
|
21
|
+
nl: () => import('./i18n/nl').then((module) => module.content),
|
|
22
|
+
pl: () => import('./i18n/pl').then((module) => module.content),
|
|
23
|
+
pt: () => import('./i18n/pt').then((module) => module.content),
|
|
24
|
+
ru: () => import('./i18n/ru').then((module) => module.content),
|
|
25
|
+
sv: () => import('./i18n/sv').then((module) => module.content),
|
|
26
|
+
tr: () => import('./i18n/tr').then((module) => module.content),
|
|
27
|
+
zh: () => import('./i18n/zh').then((module) => module.content),
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export { bibliography } from './bibliography';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SalaryCostResult } from './logic';
|
|
2
|
+
|
|
3
|
+
export type CostGapState = 'lean' | 'typical' | 'wide';
|
|
4
|
+
|
|
5
|
+
export function evaluateCostGap(result: SalaryCostResult): CostGapState {
|
|
6
|
+
if (result.costMultiplier < 1.32) return 'lean';
|
|
7
|
+
if (result.costMultiplier <= 1.38) return 'typical';
|
|
8
|
+
return 'wide';
|
|
9
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
|
|
2
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
3
|
+
import { bibliography } from '../bibliography';
|
|
4
|
+
import type { SalaryEmployerCostUI } from '../ui';
|
|
5
|
+
|
|
6
|
+
const slug = "spanien-brutto-netto-gehaltsrechner-arbeitgeberkosten";
|
|
7
|
+
const title = "Spanien Brutto Netto Gehaltsrechner & Arbeitgeberkosten 2026";
|
|
8
|
+
const description = "Berechnen Sie Nettogehalt, Einkommensteuer, Arbeitnehmerbeiträge und die Gesamtkosten des Arbeitgebers in Spanien nach den Regeln von 2026.";
|
|
9
|
+
|
|
10
|
+
const faq = [
|
|
11
|
+
{
|
|
12
|
+
"question": "Wie viel kostet ein Arbeitnehmer ein Unternehmen in Spanien?",
|
|
13
|
+
"answer": "Das Unternehmen zahlt das Bruttogehalt zuzüglich seiner Sozialversicherungsbeiträge. Für eine unbefristete Bürostelle mit 30.000 € Jahresbrutto, 1,50 % Unfallprämie und den Sätzen von 2026 betragen die geschätzten Jahreskosten für den Arbeitgeber 39.645 € vor Boni, Zusatzleistungen oder Abfindungsrückstellungen."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"question": "Wie berechne ich das Bruttogehalt zum Nettogehalt in Spanien?",
|
|
17
|
+
"answer": "Gehen Sie vom Jahresbruttogehalt aus, ziehen Sie die Sozialversicherungsbeiträge des Arbeitnehmers ab und ziehen Sie den IRPF-Einbehalt ab. Geben Sie den IRPF-Prozentsatz aus dem Rechner der Steuerbehörde, einem Angebot oder einer Gehaltsabrechnung ein."
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"question": "Verändert die Wahl von 12 oder 14 Zahlungen das Jahresgehalt?",
|
|
21
|
+
"answer": "Nein. Der Rechner behält den gleichen Bruttojahresbetrag bei und teilt die Jahresschätzungen durch 12 oder 14. Die Zahl pro Zahlung ist ein Durchschnitt."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"question": "Welche Sozialversicherungssätze sind für 2026 enthalten?",
|
|
25
|
+
"answer": "Die Schätzung umfasst allgemeine Rechnungslegung, Arbeitslosigkeit, Ausbildung, den MEI-Mechanismus und gegebenenfalls FOGASA sowie die gewählte Berufsgenossenschaftsprämie."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"question": "Was passiert, wenn das Gehalt die Beitragsbemessungsgrenze überschreitet?",
|
|
29
|
+
"answer": "Standardbeiträge sind auf die monatliche Höchstbemessungsgrenze 2026 von 5.101,20 € gedeckelt. Darüber hinaus gilt der progressive Solidaritätsbeitrag 2026."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"question": "Ist dieser Rechner außerhalb Spaniens gültig?",
|
|
33
|
+
"answer": "Nein. Er ist bewusst auf Spanien und das Allgemeine Sozialversicherungssystem 2026 für Vollzeitkräfte beschränkt."
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"question": "Werden Gehaltsinformationen an einen Server gesendet?",
|
|
37
|
+
"answer": "Nein. Die Berechnungen laufen lokal in Ihrem Browser."
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const howTo = [
|
|
42
|
+
{
|
|
43
|
+
"name": "Bruttogehalt eingeben",
|
|
44
|
+
"text": "Wählen Sie die jährliche oder monatliche Eingabe und geben Sie das Bruttogehalt ein."
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "Spanische Lohnannahmen festlegen",
|
|
48
|
+
"text": "Wählen Sie 12 oder 14 Zahlungen, unbefristeten oder befristeten Vertrag, IRPF-Einbehalt und Unfallprämie."
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "Gehaltsorbit ablesen",
|
|
52
|
+
"text": "Vergleichen Sie Nettokern, Arbeitnehmerabzüge und den Arbeitgeberbeitragshalo."
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "Ergebniszeitraum umschalten",
|
|
56
|
+
"text": "Zeigen Sie Jahressummen oder Durchschnittsbeträge pro Zahlung an."
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const faqSchema: WithContext<FAQPage> = {
|
|
61
|
+
'@context': 'https://schema.org',
|
|
62
|
+
'@type': 'FAQPage',
|
|
63
|
+
mainEntity: faq.map((item) => ({
|
|
64
|
+
'@type': 'Question',
|
|
65
|
+
name: item.question,
|
|
66
|
+
acceptedAnswer: { '@type': 'Answer', text: item.answer },
|
|
67
|
+
})),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const howToSchema: WithContext<HowTo> = {
|
|
71
|
+
'@context': 'https://schema.org',
|
|
72
|
+
'@type': 'HowTo',
|
|
73
|
+
name: title,
|
|
74
|
+
description,
|
|
75
|
+
step: howTo.map((step, index) => ({
|
|
76
|
+
'@type': 'HowToStep',
|
|
77
|
+
position: index + 1,
|
|
78
|
+
name: step.name,
|
|
79
|
+
text: step.text,
|
|
80
|
+
})),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const appSchema: WithContext<SoftwareApplication> = {
|
|
84
|
+
'@context': 'https://schema.org',
|
|
85
|
+
'@type': 'SoftwareApplication',
|
|
86
|
+
name: title,
|
|
87
|
+
description,
|
|
88
|
+
applicationCategory: 'FinanceApplication',
|
|
89
|
+
operatingSystem: 'All',
|
|
90
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
|
|
91
|
+
inLanguage: "de",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const content: ToolLocaleContent<SalaryEmployerCostUI> = {
|
|
95
|
+
slug,
|
|
96
|
+
title,
|
|
97
|
+
description,
|
|
98
|
+
ui: {
|
|
99
|
+
"labelGrossSalary": "Bruttogehalts-Signal",
|
|
100
|
+
"labelGrossSalaryHint": "Gehaltsbetrag",
|
|
101
|
+
"labelAnnual": "Jährlich",
|
|
102
|
+
"labelMonthly": "Monatlich",
|
|
103
|
+
"labelPaySchedule": "Zahlungsplan",
|
|
104
|
+
"labelTwelvePayments": "12 Zahlungen",
|
|
105
|
+
"labelFourteenPayments": "14 Zahlungen",
|
|
106
|
+
"labelContract": "Vertragsart",
|
|
107
|
+
"labelPermanent": "Unbefristet",
|
|
108
|
+
"labelTemporary": "Befristet",
|
|
109
|
+
"labelIrpfRate": "IRPF-Einbehalt (Einkommensteuer)",
|
|
110
|
+
"labelIrpfHint": "Verwenden Sie den Satz des Finanzamts, Angebots oder Lohnzettel",
|
|
111
|
+
"labelRiskRate": "Berufsgenossenschaftsprämie (Unfall u. Berufskrankheit)",
|
|
112
|
+
"labelRiskHint": "Arbeitgebersatz für Arbeitsunfälle und Berufskrankheiten",
|
|
113
|
+
"labelOfficePreset": "Büro 1,50 %",
|
|
114
|
+
"labelTradePreset": "Außendienst / Vertrieb 2,00 %",
|
|
115
|
+
"labelConstructionPreset": "Baugewerbe 6,70 %",
|
|
116
|
+
"labelNetCore": "Nettogehaltskern",
|
|
117
|
+
"labelEmployeeDeductions": "Arbeitnehmerabzüge",
|
|
118
|
+
"labelDeductionsFormula": "IRPF plus Arbeitnehmer-Sozialversicherung",
|
|
119
|
+
"labelEmployerHalo": "Arbeitgeberbeitragshalo",
|
|
120
|
+
"labelEmployerCost": "Gesamte Arbeitgeberkosten",
|
|
121
|
+
"labelAnnualView": "Jahresansicht",
|
|
122
|
+
"labelPerPaymentView": "Durchschnitt pro Zahlung",
|
|
123
|
+
"labelGross": "Bruttogehalt",
|
|
124
|
+
"labelIrpf": "IRPF-Einbehalt",
|
|
125
|
+
"labelEmployeeSocialSecurity": "Arbeitnehmer-Sozialversicherung",
|
|
126
|
+
"labelContributionBase": "Monatliche Beitragsbemessungsgrenze",
|
|
127
|
+
"labelCostMultiplier": "Kostenmultiplikator gegenüber Brutto",
|
|
128
|
+
"labelWorkerKeeps": "Nettoanteil an den Gesamtkosten",
|
|
129
|
+
"labelLeanGap": "Kompakter Kostenhalo",
|
|
130
|
+
"labelTypicalGap": "Typischer Kostenhalo",
|
|
131
|
+
"labelWideGap": "Erweiterter Kostenhalo",
|
|
132
|
+
"labelModelScope": "Nur Spanien. Allgemeines System 2026, Vollzeitbeschäftigung und ordentliches Bargeldgehalt. Die Ergebnisse sind Schätzungen, keine Lohn- oder Steuerberatung.",
|
|
133
|
+
"labelReset": "Beispiel zurücksetzen",
|
|
134
|
+
"labelInvalid": "Geben Sie ein gültiges Vollzeit-Jahresgehalt ein und halten Sie IRPF- und Unfallquoten im zulässigen Bereich.",
|
|
135
|
+
"labelMinimumSalary": "Das Modell beginnt beim Vollzeit-Mindestlohn 2026 von 17.094 € pro Jahr.",
|
|
136
|
+
"labelPrivacy": "Lokal in Ihrem Browser berechnet"
|
|
137
|
+
},
|
|
138
|
+
seo: [
|
|
139
|
+
{ type: 'title', text: "Vom Bruttogehalt zu den tatsächlichen Unternehmenskosten in Spanien", level: 2 },
|
|
140
|
+
{ type: 'paragraph', html: "Berechnen Sie Nettogehalt, Einkommensteuer, Arbeitnehmerbeiträge und die Gesamtkosten des Arbeitgebers in Spanien nach den Regeln von 2026." },
|
|
141
|
+
{ type: 'title', text: "Die Formel der Arbeitgeberkosten", level: 2 },
|
|
142
|
+
{ type: 'paragraph', html: "Das Unternehmen zahlt das Bruttogehalt zuzüglich seiner Sozialversicherungsbeiträge. Für eine unbefristete Bürostelle mit 30.000 € Jahresbrutto, 1,50 % Unfallprämie und den Sätzen von 2026 betragen die geschätzten Jahreskosten für den Arbeitgeber 39.645 € vor Boni, Zusatzleistungen oder Abfindungsrückstellungen." },
|
|
143
|
+
{ type: 'table', headers: ['Layer', 'Details'], rows: [['Net', "Nettogehaltskern"], ['Employer Cost', "Gesamte Arbeitgeberkosten"]] },
|
|
144
|
+
{ type: 'title', text: "Enthaltene spanische Sozialversicherungssätze für 2026", level: 2 },
|
|
145
|
+
{ type: 'paragraph', html: "Die Schätzung umfasst allgemeine Rechnungslegung, Arbeitslosigkeit, Ausbildung, den MEI-Mechanismus und gegebenenfalls FOGASA sowie die gewählte Berufsgenossenschaftsprämie." },
|
|
146
|
+
{ type: 'table', headers: ['Preset', 'Rate'], rows: [["Büro 1,50 %", '1.50%'], ["Außendienst / Vertrieb 2,00 %", '2.00%'], ["Baugewerbe 6,70 %", '6.70%']] },
|
|
147
|
+
{ type: 'title', text: "Beitragsgrenzen und hohe Gehälter", level: 2 },
|
|
148
|
+
{ type: 'paragraph', html: "Standardbeiträge sind auf die monatliche Höchstbemessungsgrenze 2026 von 5.101,20 € gedeckelt. Darüber hinaus gilt der progressive Solidaritätsbeitrag 2026." },
|
|
149
|
+
{ type: 'title', text: "Zwölf Zahlungen und vierzehn Zahlungen", level: 2 },
|
|
150
|
+
{ type: 'paragraph', html: "Nein. Der Rechner behält den gleichen Bruttojahresbetrag bei und teilt die Jahresschätzungen durch 12 oder 14. Die Zahl pro Zahlung ist ein Durchschnitt." },
|
|
151
|
+
{ type: 'tip', title: "Zahlungsplan", html: "Wählen Sie 12 oder 14 Zahlungen, unbefristeten oder befristeten Vertrag, IRPF-Einbehalt und Unfallprämie." },
|
|
152
|
+
{ type: 'title', text: "Grenzen dieser spanischen Gehaltsschätzung", level: 2 },
|
|
153
|
+
{ type: 'paragraph', html: "Nur Spanien. Allgemeines System 2026, Vollzeitbeschäftigung und ordentliches Bargeldgehalt. Die Ergebnisse sind Schätzungen, keine Lohn- oder Steuerberatung." },
|
|
154
|
+
],
|
|
155
|
+
faq,
|
|
156
|
+
bibliography,
|
|
157
|
+
howTo,
|
|
158
|
+
schemas: [faqSchema, howToSchema, appSchema],
|
|
159
|
+
};
|