@jjlmoya/utils-finance 1.28.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 +3 -1
- package/src/entries.ts +7 -1
- package/src/index.ts +2 -1
- package/src/tests/locale_completeness.test.ts +2 -3
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/pesetaValue/bibliography.astro +6 -0
- package/src/tool/pesetaValue/bibliography.ts +16 -0
- package/src/tool/pesetaValue/component.astro +86 -0
- package/src/tool/pesetaValue/controller.ts +126 -0
- package/src/tool/pesetaValue/dom-views.ts +83 -0
- package/src/tool/pesetaValue/entry.ts +31 -0
- package/src/tool/pesetaValue/evaluator.ts +18 -0
- package/src/tool/pesetaValue/i18n/de.ts +54 -0
- package/src/tool/pesetaValue/i18n/en.ts +138 -0
- package/src/tool/pesetaValue/i18n/es.ts +54 -0
- package/src/tool/pesetaValue/i18n/fr.ts +54 -0
- package/src/tool/pesetaValue/i18n/id.ts +48 -0
- package/src/tool/pesetaValue/i18n/it.ts +54 -0
- package/src/tool/pesetaValue/i18n/ja.ts +48 -0
- package/src/tool/pesetaValue/i18n/ko.ts +48 -0
- package/src/tool/pesetaValue/i18n/nl.ts +48 -0
- package/src/tool/pesetaValue/i18n/pl.ts +48 -0
- package/src/tool/pesetaValue/i18n/pt.ts +48 -0
- package/src/tool/pesetaValue/i18n/ru.ts +48 -0
- package/src/tool/pesetaValue/i18n/sv.ts +48 -0
- package/src/tool/pesetaValue/i18n/tr.ts +48 -0
- package/src/tool/pesetaValue/i18n/zh.ts +48 -0
- package/src/tool/pesetaValue/index.ts +11 -0
- package/src/tool/pesetaValue/logic.test.ts +63 -0
- package/src/tool/pesetaValue/logic.ts +162 -0
- package/src/tool/pesetaValue/pesetas-to-euros-with-inflation-calculator.css +564 -0
- package/src/tool/pesetaValue/seo.astro +15 -0
- package/src/tool/pesetaValue/storage.ts +25 -0
- package/src/tool/pesetaValue/ui.ts +33 -0
- 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 +3 -3
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -14,10 +14,12 @@ import { legalInterestRate } from '../tool/legalInterestRate/entry';
|
|
|
14
14
|
import { fireCalculator } from '../tool/fireCalculator/entry';
|
|
15
15
|
import { debtSnowball } from '../tool/debtSnowball/entry';
|
|
16
16
|
import { revolvingCardCalculator } from '../tool/revolvingCardCalculator/entry';
|
|
17
|
+
import { pesetaValue } from '../tool/pesetaValue/entry';
|
|
18
|
+
import { salaryEmployerCost } from '../tool/salaryEmployerCost/entry';
|
|
17
19
|
|
|
18
20
|
export const financeCategory: FinanceCategoryEntry = {
|
|
19
21
|
icon: 'mdi:finance',
|
|
20
|
-
tools: [compoundInterest, financialFreedom, profitability, mortgage, inflation, percentageCalculator, lateInterest, ibanBicSwift, rentIncrease, lotteryOptimizer, courtFeeCalculator, legalInterestRate, fireCalculator, debtSnowball, revolvingCardCalculator],
|
|
22
|
+
tools: [compoundInterest, financialFreedom, profitability, mortgage, inflation, percentageCalculator, lateInterest, ibanBicSwift, rentIncrease, lotteryOptimizer, courtFeeCalculator, legalInterestRate, fireCalculator, debtSnowball, revolvingCardCalculator, pesetaValue, salaryEmployerCost],
|
|
21
23
|
i18n: {
|
|
22
24
|
en: () => import('./i18n/en').then((m) => m.content),
|
|
23
25
|
es: () => import('./i18n/es').then((m) => m.content),
|
package/src/entries.ts
CHANGED
|
@@ -28,6 +28,10 @@ export { rentIncrease } from './tool/rentIncreaseCalculator/entry';
|
|
|
28
28
|
export type { RentIncreaseLocaleContent } from './tool/rentIncreaseCalculator/entry';
|
|
29
29
|
export { revolvingCardCalculator } from './tool/revolvingCardCalculator/entry';
|
|
30
30
|
export type { RevolvingCardCalculatorLocaleContent } from './tool/revolvingCardCalculator/entry';
|
|
31
|
+
export { pesetaValue } from './tool/pesetaValue/entry';
|
|
32
|
+
export type { PesetaValueLocaleContent } from './tool/pesetaValue/entry';
|
|
33
|
+
export { salaryEmployerCost } from './tool/salaryEmployerCost/entry';
|
|
34
|
+
export type { SalaryEmployerCostLocaleContent } from './tool/salaryEmployerCost/entry';
|
|
31
35
|
export { financeCategory } from './category';
|
|
32
36
|
import { compoundInterest } from './tool/compoundInterest/entry';
|
|
33
37
|
import { financialFreedom } from './tool/financialFreedom/entry';
|
|
@@ -44,4 +48,6 @@ import { mortgage } from './tool/mortgage/entry';
|
|
|
44
48
|
import { percentageCalculator } from './tool/percentageCalculator/entry';
|
|
45
49
|
import { rentIncrease } from './tool/rentIncreaseCalculator/entry';
|
|
46
50
|
import { revolvingCardCalculator } from './tool/revolvingCardCalculator/entry';
|
|
47
|
-
|
|
51
|
+
import { pesetaValue } from './tool/pesetaValue/entry';
|
|
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
|
@@ -30,4 +30,5 @@ export { LEGAL_INTEREST_RATE_TOOL } from './tool/legalInterestRate';
|
|
|
30
30
|
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: 'INE CPI national index series from 1961',
|
|
6
|
+
url: 'https://www.ine.es/jaxiT3/Tabla.htm?L=0&t=24077',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'Official irrevocable euro conversion rates',
|
|
10
|
+
url: 'https://www.boe.es/doue/1998/359/L00001-00002.pdf',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'Banco de España history of the peseta and its denominations',
|
|
14
|
+
url: 'https://www.bde.es/wbe/es/areas-actuacion/billetes-monedas/historia-peseta/',
|
|
15
|
+
},
|
|
16
|
+
];
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { PesetaValueUI } from './ui';
|
|
3
|
+
import { getSupportedYears, REFERENCE_YEAR } from './logic';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
ui: PesetaValueUI;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { ui } = Astro.props;
|
|
10
|
+
const years = getSupportedYears();
|
|
11
|
+
const decades = [...new Set(years.map((year) => Math.floor(year / 10) * 10))];
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<div class="pv-tool" data-peseta-value>
|
|
15
|
+
<script is:inline type="application/json" data-peseta-config set:html={JSON.stringify(ui)}></script>
|
|
16
|
+
<div class="pv-stage">
|
|
17
|
+
<div class="pv-controls">
|
|
18
|
+
<div class="pv-control-heading">
|
|
19
|
+
<span class="pv-eyebrow">{ui.labelAmountHint}</span>
|
|
20
|
+
<span class="pv-amount-label" data-amount-label>1,000 {ui.labelPesetas}</span>
|
|
21
|
+
</div>
|
|
22
|
+
<label class="pv-label" for="pv-amount">{ui.labelAmount}</label>
|
|
23
|
+
<div class="pv-amount-field">
|
|
24
|
+
<input id="pv-amount" data-peseta-input type="number" min="1" step="1" value="1000" inputmode="numeric" />
|
|
25
|
+
<span>{ui.labelPesetas}</span>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="pv-presets" aria-label={ui.labelAmountHint}>
|
|
28
|
+
<button type="button" data-preset="100">{ui.labelPreset100}</button>
|
|
29
|
+
<button type="button" data-preset="1000" class="is-active">{ui.labelPreset1000}</button>
|
|
30
|
+
<button type="button" data-preset="10000">{ui.labelPreset10000}</button>
|
|
31
|
+
<button type="button" data-preset="100000">{ui.labelPreset100000}</button>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="pv-year-control">
|
|
34
|
+
<span class="pv-label">{ui.labelOriginYear}</span>
|
|
35
|
+
<div class="pv-select" data-year-select>
|
|
36
|
+
<button type="button" class="pv-select-trigger" data-year-trigger aria-expanded="false" aria-haspopup="listbox">2000</button>
|
|
37
|
+
<div class="pv-year-menu" data-year-menu role="listbox" aria-label={ui.labelOpenYears}>
|
|
38
|
+
<div class="pv-decade-nav" role="tablist" aria-label={ui.labelDecade}>
|
|
39
|
+
{decades.map((decade) => <button type="button" data-decade={decade} class:is-active={decade === 2000} role="tab" aria-selected={decade === 2000}>{decade}s</button>)}
|
|
40
|
+
</div>
|
|
41
|
+
<div class="pv-year-grid">
|
|
42
|
+
{years.map((year) => <button type="button" data-year-option data-year={year} data-decade={Math.floor(year / 10) * 10} class:is-selected={year === 2000} role="option" aria-selected={year === 2000} hidden={Math.floor(year / 10) * 10 !== 2000}>{year}</button>)}
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<input type="hidden" data-year-value value="2000" />
|
|
47
|
+
<span class="pv-help">{ui.labelOriginYearHint}</span>
|
|
48
|
+
</div>
|
|
49
|
+
<button type="button" class="pv-reset" data-reset>{ui.labelReset}</button>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div class="pv-visual" aria-live="polite">
|
|
53
|
+
<div class="pv-skyline" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span></div>
|
|
54
|
+
<div class="pv-cash-scene" data-cash-scene>
|
|
55
|
+
<div class="pv-cash-pieces" data-cash-pieces aria-label={ui.labelCashPieces}>
|
|
56
|
+
<div class="pv-cash-piece pv-cash-banknote pv-cash-value-1000" data-cash-initial aria-hidden="true"><strong>1,000</strong></div>
|
|
57
|
+
</div>
|
|
58
|
+
<span class="pv-cash-caption" data-cash-caption>1,000 {ui.labelPesetas}</span>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="pv-orbit"><span class="pv-orbit-dot"></span></div>
|
|
61
|
+
<div class="pv-visual-caption" aria-label={ui.labelReferenceYear}><span>{ui.labelFrom} <strong data-result-origin>2000</strong></span><span>{ui.labelTo} <strong data-result-reference>{REFERENCE_YEAR}</strong></span></div>
|
|
62
|
+
<div class="pv-state"><span class="pv-state-dot"></span><span data-result-state-label>{ui.labelState}</span></div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div class="pv-results">
|
|
67
|
+
<div class="pv-result-main">
|
|
68
|
+
<span class="pv-result-kicker">{ui.labelTodayValue}</span>
|
|
69
|
+
<strong data-result-adjusted aria-label={ui.labelEuro}>€0.00</strong>
|
|
70
|
+
<span class="pv-result-note" data-result-change aria-label={ui.labelPurchasingPowerChange}>0.0%</span>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="pv-result-grid">
|
|
73
|
+
<div><span>{ui.labelNominalEuros}</span><strong data-result-nominal>€0.00</strong></div>
|
|
74
|
+
<div><span>{ui.labelNominalDifference}</span><strong data-result-difference>€0.00</strong></div>
|
|
75
|
+
<div><span>{ui.labelConversionRate}</span><strong>166.386</strong></div>
|
|
76
|
+
<div><span>{ui.labelCpiMethod}</span><strong data-result-cpi>1.00×</strong></div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<script>
|
|
82
|
+
import { mountPesetaValue } from './controller';
|
|
83
|
+
const root = document.querySelector<HTMLElement>('[data-peseta-value]');
|
|
84
|
+
const config = root?.querySelector('[data-peseta-config]');
|
|
85
|
+
if (root && config?.textContent) mountPesetaValue(root, JSON.parse(config.textContent));
|
|
86
|
+
</script>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { calculatePesetaValue, getSupportedYears, REFERENCE_YEAR } from './logic';
|
|
2
|
+
import { renderEmptyPesetaValue, renderPesetaValue, formatPesetas } from './dom-views';
|
|
3
|
+
import { readPesetaValueState, writePesetaValueState } from './storage';
|
|
4
|
+
import type { PesetaValueUI } from './ui';
|
|
5
|
+
|
|
6
|
+
interface ControllerState {
|
|
7
|
+
pesetas: number;
|
|
8
|
+
originYear: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function setActiveDecade(root: HTMLElement, decade: number): void {
|
|
12
|
+
root.querySelectorAll<HTMLButtonElement>('[data-decade][role="tab"]').forEach((button) => {
|
|
13
|
+
const isActive = Number(button.dataset.decade) === decade;
|
|
14
|
+
button.classList.toggle('is-active', isActive);
|
|
15
|
+
button.setAttribute('aria-selected', String(isActive));
|
|
16
|
+
});
|
|
17
|
+
root.querySelectorAll<HTMLButtonElement>('[data-year-option]').forEach((option) => {
|
|
18
|
+
option.hidden = Number(option.dataset.decade) !== decade;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getInput(root: HTMLElement): HTMLInputElement | null {
|
|
23
|
+
return root.querySelector<HTMLInputElement>('[data-peseta-input]');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function setYearMenu(root: HTMLElement, year: number, ui: PesetaValueUI): void {
|
|
27
|
+
const trigger = root.querySelector<HTMLButtonElement>('[data-year-trigger]');
|
|
28
|
+
const hidden = root.querySelector<HTMLInputElement>('[data-year-value]');
|
|
29
|
+
const options = root.querySelectorAll<HTMLButtonElement>('[data-year-option]');
|
|
30
|
+
if (trigger) trigger.textContent = String(year);
|
|
31
|
+
if (hidden) hidden.value = String(year);
|
|
32
|
+
options.forEach((option) => option.classList.toggle('is-selected', Number(option.dataset.year) === year));
|
|
33
|
+
setActiveDecade(root, Math.floor(year / 10) * 10);
|
|
34
|
+
trigger?.setAttribute('aria-label', `${ui.labelOriginYear}: ${year}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function closeYearMenu(root: HTMLElement, ui: PesetaValueUI): void {
|
|
38
|
+
const menu = root.querySelector<HTMLElement>('[data-year-menu]');
|
|
39
|
+
const trigger = root.querySelector<HTMLButtonElement>('[data-year-trigger]');
|
|
40
|
+
menu?.classList.remove('is-open');
|
|
41
|
+
menu?.setAttribute('aria-label', ui.labelOpenYears);
|
|
42
|
+
trigger?.setAttribute('aria-expanded', 'false');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toggleYearMenu(root: HTMLElement, ui: PesetaValueUI): void {
|
|
46
|
+
const menu = root.querySelector<HTMLElement>('[data-year-menu]');
|
|
47
|
+
const trigger = root.querySelector<HTMLButtonElement>('[data-year-trigger]');
|
|
48
|
+
const isOpen = menu?.classList.toggle('is-open') ?? false;
|
|
49
|
+
menu?.setAttribute('aria-label', isOpen ? ui.labelCloseYears : ui.labelOpenYears);
|
|
50
|
+
trigger?.setAttribute('aria-expanded', String(isOpen));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readState(root: HTMLElement): ControllerState {
|
|
54
|
+
const input = getInput(root);
|
|
55
|
+
const year = Number(root.querySelector<HTMLInputElement>('[data-year-value]')?.value ?? REFERENCE_YEAR);
|
|
56
|
+
return { pesetas: Number(input?.value ?? 1000), originYear: year };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function update(root: HTMLElement, ui: PesetaValueUI): void {
|
|
60
|
+
const state = readState(root);
|
|
61
|
+
const result = calculatePesetaValue(state);
|
|
62
|
+
if (result) {
|
|
63
|
+
renderPesetaValue(root, result, ui);
|
|
64
|
+
writePesetaValueState(state);
|
|
65
|
+
} else {
|
|
66
|
+
renderEmptyPesetaValue(root, ui);
|
|
67
|
+
}
|
|
68
|
+
const input = getInput(root);
|
|
69
|
+
const amountLabel = root.querySelector<HTMLElement>('[data-amount-label]');
|
|
70
|
+
if (input && amountLabel) amountLabel.textContent = `${formatPesetas(Number(input.value) || 0)} ${ui.labelPesetas}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function bindPresets(root: HTMLElement, ui: PesetaValueUI): void {
|
|
74
|
+
root.querySelectorAll<HTMLButtonElement>('[data-preset]').forEach((button) => {
|
|
75
|
+
button.addEventListener('click', () => {
|
|
76
|
+
const input = getInput(root);
|
|
77
|
+
if (input) input.value = button.dataset.preset ?? '1000';
|
|
78
|
+
root.querySelectorAll('[data-preset]').forEach((item) => item.classList.toggle('is-active', item === button));
|
|
79
|
+
update(root, ui);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function bindYearMenu(root: HTMLElement, ui: PesetaValueUI): void {
|
|
85
|
+
root.querySelector('[data-year-trigger]')?.addEventListener('click', () => toggleYearMenu(root, ui));
|
|
86
|
+
root.querySelectorAll<HTMLButtonElement>('[data-decade][role="tab"]').forEach((button) => {
|
|
87
|
+
button.addEventListener('click', () => setActiveDecade(root, Number(button.dataset.decade)));
|
|
88
|
+
});
|
|
89
|
+
root.querySelectorAll<HTMLButtonElement>('[data-year-option]').forEach((option) => {
|
|
90
|
+
option.addEventListener('click', () => {
|
|
91
|
+
setYearMenu(root, Number(option.dataset.year), ui);
|
|
92
|
+
closeYearMenu(root, ui);
|
|
93
|
+
update(root, ui);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
document.addEventListener('click', (event) => {
|
|
97
|
+
if (!root.contains(event.target as Node)) closeYearMenu(root, ui);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resetTool(root: HTMLElement, input: HTMLInputElement | null, ui: PesetaValueUI): void {
|
|
102
|
+
if (input) input.value = '1000';
|
|
103
|
+
setYearMenu(root, 2000, ui);
|
|
104
|
+
root.querySelectorAll('[data-preset]').forEach((item) => item.classList.remove('is-active'));
|
|
105
|
+
update(root, ui);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function restoreSavedState(root: HTMLElement, input: HTMLInputElement | null, ui: PesetaValueUI): void {
|
|
109
|
+
const saved = readPesetaValueState();
|
|
110
|
+
if (input) input.value = String(saved?.pesetas ?? 1000);
|
|
111
|
+
setYearMenu(root, saved?.originYear ?? 2000, ui);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function mountPesetaValue(root: HTMLElement, ui: PesetaValueUI): void {
|
|
115
|
+
const input = getInput(root);
|
|
116
|
+
restoreSavedState(root, input, ui);
|
|
117
|
+
const changeLabel = root.querySelector<HTMLElement>('[data-result-change]');
|
|
118
|
+
changeLabel?.setAttribute('aria-label', ui.labelPurchasingPowerChange);
|
|
119
|
+
root.querySelector('[data-reset]')?.addEventListener('click', () => resetTool(root, input, ui));
|
|
120
|
+
input?.addEventListener('input', () => update(root, ui));
|
|
121
|
+
bindPresets(root, ui);
|
|
122
|
+
bindYearMenu(root, ui);
|
|
123
|
+
update(root, ui);
|
|
124
|
+
root.dataset.referenceYear = String(REFERENCE_YEAR);
|
|
125
|
+
root.dataset.availableYears = String(getSupportedYears().length);
|
|
126
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { PesetaValueUI } from './ui';
|
|
2
|
+
import { decomposePesetas, type CashPiece, type PesetaValueResult } from './logic';
|
|
3
|
+
import { evaluatePesetaValue, type ValueState } from './evaluator';
|
|
4
|
+
|
|
5
|
+
export function formatCurrency(value: number, ui: PesetaValueUI): string {
|
|
6
|
+
return new Intl.NumberFormat(ui.currencyLocale, {
|
|
7
|
+
style: 'currency',
|
|
8
|
+
currency: 'EUR',
|
|
9
|
+
maximumFractionDigits: 2,
|
|
10
|
+
}).format(value).replace(/\u00a0/g, ' ');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function formatPesetas(value: number): string {
|
|
14
|
+
return new Intl.NumberFormat('en-GB', { maximumFractionDigits: 0 }).format(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function renderCashPiece(piece: CashPiece, ui: PesetaValueUI): HTMLElement {
|
|
18
|
+
const element = document.createElement('div');
|
|
19
|
+
const label = piece.kind === 'coin' ? ui.labelCoin : ui.labelBanknote;
|
|
20
|
+
element.className = `pv-cash-piece pv-cash-${piece.kind} pv-cash-value-${piece.value}${piece.count > 0 ? ' is-used' : ''}`;
|
|
21
|
+
element.dataset.value = String(piece.value);
|
|
22
|
+
element.setAttribute('aria-label', `${piece.count} ${label} ${formatPesetas(piece.value)} ${ui.labelPesetas}`);
|
|
23
|
+
element.dataset.count = String(piece.count);
|
|
24
|
+
const value = document.createElement('strong');
|
|
25
|
+
value.textContent = formatPesetas(piece.value);
|
|
26
|
+
element.append(value);
|
|
27
|
+
if (piece.count > 1) {
|
|
28
|
+
const count = document.createElement('small');
|
|
29
|
+
count.textContent = `${piece.count}×`;
|
|
30
|
+
element.append(count);
|
|
31
|
+
}
|
|
32
|
+
return element;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function renderCashScene(root: HTMLElement, result: PesetaValueResult, ui: PesetaValueUI): void {
|
|
36
|
+
const piecesRoot = root.querySelector<HTMLElement>('[data-cash-pieces]');
|
|
37
|
+
const caption = root.querySelector<HTMLElement>('[data-cash-caption]');
|
|
38
|
+
if (!piecesRoot || !caption) return;
|
|
39
|
+
piecesRoot.replaceChildren(...decomposePesetas(result.pesetas).map((piece) => renderCashPiece(piece, ui)));
|
|
40
|
+
caption.textContent = `${formatPesetas(result.pesetas)} ${ui.labelPesetas}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function setText(root: HTMLElement, selector: string, value: string): void {
|
|
44
|
+
root.querySelectorAll<HTMLElement>(selector).forEach((element) => {
|
|
45
|
+
element.textContent = value;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function renderDial(root: HTMLElement, result: PesetaValueResult): void {
|
|
50
|
+
const evaluation = evaluatePesetaValue(result);
|
|
51
|
+
const progress = Math.min(100, Math.max(8, result.cpiMultiplier * 12));
|
|
52
|
+
root.style.setProperty('--pv-progress', `${progress}%`);
|
|
53
|
+
root.dataset.state = evaluation.state;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getStateLabel(state: ValueState, ui: PesetaValueUI): string {
|
|
57
|
+
if (state === 'positive') return ui.labelStatePositive;
|
|
58
|
+
if (state === 'negative') return ui.labelStateNegative;
|
|
59
|
+
return ui.labelStateNeutral;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function renderPesetaValue(root: HTMLElement, result: PesetaValueResult, ui: PesetaValueUI): void {
|
|
63
|
+
const evaluation = evaluatePesetaValue(result);
|
|
64
|
+
setText(root, '[data-result-nominal]', formatCurrency(result.nominalEuros, ui));
|
|
65
|
+
setText(root, '[data-result-adjusted]', formatCurrency(result.adjustedEuros, ui));
|
|
66
|
+
setText(root, '[data-result-difference]', formatCurrency(result.nominalDifference, ui));
|
|
67
|
+
setText(root, '[data-result-change]', `${result.purchasingPowerChange > 0 ? '+' : ''}${result.purchasingPowerChange.toFixed(1)}%`);
|
|
68
|
+
setText(root, '[data-result-origin]', String(result.originYear));
|
|
69
|
+
setText(root, '[data-result-reference]', String(result.referenceYear));
|
|
70
|
+
setText(root, '[data-result-cpi]', `${result.cpiMultiplier.toFixed(2)}×`);
|
|
71
|
+
setText(root, '[data-result-state-label]', getStateLabel(evaluation.state, ui));
|
|
72
|
+
renderCashScene(root, result, ui);
|
|
73
|
+
renderDial(root, result);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function renderEmptyPesetaValue(root: HTMLElement, ui: PesetaValueUI): void {
|
|
77
|
+
setText(root, '[data-result-nominal]', formatCurrency(0, ui));
|
|
78
|
+
setText(root, '[data-result-adjusted]', formatCurrency(0, ui));
|
|
79
|
+
setText(root, '[data-result-difference]', formatCurrency(0, ui));
|
|
80
|
+
setText(root, '[data-result-change]', '0.0%');
|
|
81
|
+
root.querySelector<HTMLElement>('[data-cash-pieces]')?.replaceChildren();
|
|
82
|
+
setText(root, '[data-cash-caption]', '');
|
|
83
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { FinanceToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
import type { PesetaValueUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type PesetaValueLocaleContent = ToolLocaleContent<PesetaValueUI>;
|
|
5
|
+
|
|
6
|
+
export const pesetaValue: FinanceToolEntry<PesetaValueUI> = {
|
|
7
|
+
id: 'actualizador-pesetas-ipc',
|
|
8
|
+
icons: {
|
|
9
|
+
bg: 'mdi:currency-eur',
|
|
10
|
+
fg: 'mdi:cash-multiple',
|
|
11
|
+
},
|
|
12
|
+
i18n: {
|
|
13
|
+
en: () => import('./i18n/en').then((module) => module.content),
|
|
14
|
+
de: () => import('./i18n/de').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,18 @@
|
|
|
1
|
+
import type { PesetaValueResult } from './logic';
|
|
2
|
+
|
|
3
|
+
export type ValueState = 'positive' | 'neutral' | 'negative';
|
|
4
|
+
|
|
5
|
+
export interface ValueEvaluation {
|
|
6
|
+
state: ValueState;
|
|
7
|
+
ratioLabel: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function evaluatePesetaValue(result: PesetaValueResult): ValueEvaluation {
|
|
11
|
+
if (result.purchasingPowerChange > 0.5) {
|
|
12
|
+
return { state: 'positive', ratioLabel: 'expanded' };
|
|
13
|
+
}
|
|
14
|
+
if (result.purchasingPowerChange < -0.5) {
|
|
15
|
+
return { state: 'negative', ratioLabel: 'contracted' };
|
|
16
|
+
}
|
|
17
|
+
return { state: 'neutral', ratioLabel: 'unchanged' };
|
|
18
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { FAQPage, HowTo, SoftwareApplication, Thing, WithContext } from 'schema-dts';
|
|
2
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
3
|
+
import { bibliography } from '../bibliography';
|
|
4
|
+
import type { PesetaValueUI } from '../ui';
|
|
5
|
+
|
|
6
|
+
const slug = 'peseten-in-euro-umrechner';
|
|
7
|
+
const title = 'Peseten in Euro Rechner: Wert alter Peseten 2026';
|
|
8
|
+
const description = 'Rechnen Sie spanische Peseten mit dem festen Kurs von 166,386 Peseten pro Euro um und vergleichen Sie die Kaufkraft bis 2026 mit dem spanischen Verbraucherpreisindex.';
|
|
9
|
+
|
|
10
|
+
const faq = [
|
|
11
|
+
{ question: 'Wie viel sind 1.000 Peseten in Euro?', answer: '1.000 Peseten entsprechen beim festen Umrechnungskurs 6,01 Euro. Die separate CPI-Berechnung zeigt zusätzlich, welche Kaufkraft dieser Betrag im gewählten Jahr hatte.' },
|
|
12
|
+
{ question: 'Wie viel sind eine Million Peseten in Euro?', answer: 'Eine Million Peseten entsprechen 6.010,12 Euro beim festen Kurs. Die Kaufkraftschätzung für 2026 hängt vom Jahr ab, in dem der Betrag verwendet wurde.' },
|
|
13
|
+
{ question: 'Wie rechnet man Peseten in Euro um?', answer: 'Teilen Sie den Pesetenbetrag durch 166,386. Zum Beispiel sind 100 Peseten 0,60 Euro und 10.000 Peseten 60,10 Euro.' },
|
|
14
|
+
{ question: 'Was wären alte Peseten im Jahr 2026 wert?', answer: 'Der Rechner zeigt den festen Eurobetrag und zusätzlich eine Schätzung der durchschnittlichen Kaufkraft in 2026 anhand des spanischen Verbraucherpreisindex.' },
|
|
15
|
+
{ question: 'Berechnet der Rechner den Sammlerwert von Münzen oder Scheinen?', answer: 'Nein. Er berechnet Währungsumrechnung und durchschnittliche Kaufkraft. Seltene Münzen und Banknoten können je nach Zustand, Seltenheit und Nachfrage einen anderen Marktwert haben.' },
|
|
16
|
+
{ question: 'Welche Jahre verwendet der Peseten Rechner?', answer: 'Als Ursprungsjahr können Sie 1980 bis 2026 wählen. Das Bezugsjahr 2026 basiert auf den neuesten verfügbaren CPI-Daten und ist noch kein vollständiger Jahresdurchschnitt.' },
|
|
17
|
+
{ question: 'Ist die Kaufkraftschätzung ein genauer Preis?', answer: 'Nein. Sie vergleicht die durchschnittliche Kaufkraft nach dem CPI und ist kein exakter Preis für Wohnen, Löhne, Lebensmittel, Energie oder einen bestimmten Kauf.' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const howTo = [
|
|
21
|
+
{ name: 'Betrag in Peseten eingeben', text: 'Geben Sie den Betrag von einer Quittung, einem Schein, einer Münze oder einem historischen Dokument ein.' },
|
|
22
|
+
{ name: 'Jahr auswählen', text: 'Wählen Sie das Jahrzehnt und anschließend das Jahr, in dem der Betrag verwendet wurde.' },
|
|
23
|
+
{ name: 'Festen Eurobetrag prüfen', text: 'Lesen Sie den nominalen Eurobetrag ab, der durch Teilen durch 166,386 entsteht.' },
|
|
24
|
+
{ name: 'Kaufkraft vergleichen', text: 'Vergleichen Sie den festen Betrag mit der geschätzten durchschnittlichen Kaufkraft für 2026.' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const schemas: WithContext<Thing>[] = [
|
|
28
|
+
{ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) } as WithContext<FAQPage>,
|
|
29
|
+
{ '@context': 'https://schema.org', '@type': 'HowTo', name: title, description, step: howTo.map((step, index) => ({ '@type': 'HowToStep', position: index + 1, name: step.name, text: step.text })) } as WithContext<HowTo>,
|
|
30
|
+
{ '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: title, description, applicationCategory: 'FinanceApplication', operatingSystem: 'All', offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' }, inLanguage: 'de' } as WithContext<SoftwareApplication>,
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export const content: ToolLocaleContent<PesetaValueUI> = {
|
|
34
|
+
slug, title, description,
|
|
35
|
+
ui: {
|
|
36
|
+
labelAmount: 'Historischer Betrag', labelAmountHint: 'Startpunkt wählen', labelOriginYear: 'Ursprungsjahr', labelOriginYearHint: 'Jährlicher CPI-Durchschnitt, sofern verfügbar', labelReferenceYear: 'Bezugsjahr 2026 bis heute', labelNominalEuros: 'Feste Umrechnung', labelTodayValue: 'Geschätzte Kaufkraft', labelPurchasingPowerChange: 'Veränderung der Kaufkraft', labelNominalDifference: 'Unterschied zur Umrechnung', labelConversionRate: 'Peseten pro Euro', labelCpiMethod: 'CPI-Multiplikator', labelPreset100: '100 Pta', labelPreset1000: '1.000 Pta', labelPreset10000: '10.000 Pta', labelPreset100000: '100.000 Pta', labelPesetas: 'Peseten', labelEuro: 'Euro', labelFrom: 'Von', labelTo: 'Bis', labelOpenYears: 'Ursprungsjahre öffnen', labelCloseYears: 'Ursprungsjahre schließen', labelDecade: 'Jahrzehnt wählen', labelCoin: 'Münze', labelBanknote: 'Banknote', labelCashPieces: 'Geldstücke', labelState: 'Wertentwicklung', labelStatePositive: 'gestiegen', labelStateNeutral: 'unverändert', labelStateNegative: 'gesunken', labelReset: 'Beispiel zurücksetzen', currencyLocale: 'de-DE',
|
|
37
|
+
},
|
|
38
|
+
seo: [
|
|
39
|
+
{ type: 'title', text: 'Peseten in Euro: Die Antwort hängt von der Frage ab', level: 2 },
|
|
40
|
+
{ type: 'paragraph', html: 'Wenn Sie einen alten Preis, Lohn, Beleg, eine Münze oder Banknote in spanischen Peseten haben, liefert dieser Rechner zwei Antworten. Er zeigt zuerst die feste Umrechnung in Euro und danach die geschätzte Kaufkraft in 2026 nach der spanischen Preisentwicklung.' },
|
|
41
|
+
{ type: 'title', text: 'Häufige Umrechnungen von Peseten in Euro', level: 2 },
|
|
42
|
+
{ type: 'paragraph', html: 'Der feste Kurs beträgt immer 166,386 Peseten für 1 Euro. Das Jahr ändert diesen nominalen Kurs nicht.' },
|
|
43
|
+
{ type: 'table', headers: ['Alter Betrag', 'Feste Umrechnung'], rows: [['1 Pesete', '0,01 Euro'], ['25 Peseten', '0,15 Euro'], ['100 Peseten', '0,60 Euro'], ['500 Peseten', '3,01 Euro'], ['1.000 Peseten', '6,01 Euro'], ['2.000 Peseten', '12,02 Euro'], ['5.000 Peseten', '30,05 Euro'], ['10.000 Peseten', '60,10 Euro'], ['1 Million Peseten', '6.010,12 Euro']] },
|
|
44
|
+
{ type: 'title', text: 'Was waren Peseten im Jahr 2026 wert?', level: 2 },
|
|
45
|
+
{ type: 'paragraph', html: 'Die CPI-Schätzung beantwortet eine andere Frage als die Euro-Umrechnung. Sie schätzt, wie viele Euro im Bezugsjahr für einen ähnlichen durchschnittlichen Warenkorb nötig wären wie für den gewählten Pesetenbetrag im Ursprungsjahr. 2026 verwendet die neuesten verfügbaren Daten bis heute und ist noch kein vollständiger Jahresdurchschnitt.' },
|
|
46
|
+
{ type: 'table', headers: ['Ergebnis', 'Bedeutung'], rows: [['Feste Umrechnung', 'Der nominale Eurobetrag nach dem offiziellen Kurs.'], ['CPI-bereinigter Wert', 'Eine Schätzung der durchschnittlichen Kaufkraft in 2026.'], ['Unterschied', 'Die Lücke zwischen Umrechnung und CPI-Schätzung.']] },
|
|
47
|
+
{ type: 'title', text: 'So berechnen Sie einen alten Pesetenbetrag', level: 2 },
|
|
48
|
+
{ type: 'list', items: ['Betrag von Beleg, Schein, Münze oder Dokument eingeben.', 'Jahr auswählen, in dem der Betrag verwendet wurde.', 'Feste Umrechnung für den direkten Eurovergleich verwenden.', 'CPI-Wert für den historischen Kaufkraftvergleich verwenden.', 'Den Unterschied als Inflationserklärung verstehen, nicht als persönliche Preisprognose.'] },
|
|
49
|
+
{ type: 'title', text: 'Münzen und Banknoten haben nicht automatisch diesen Sammlerwert', level: 2 },
|
|
50
|
+
{ type: 'paragraph', html: 'Dies ist ein Rechner für Währung und Inflation, kein Preisführer für Sammler. Eine seltene Pesetenmünze oder Banknote kann abhängig von Jahrgang, Zustand, Seltenheit und Nachfrage einen eigenen Marktwert haben.' },
|
|
51
|
+
{ type: 'tip', title: 'Das Jahr ändert die Kaufkraft, nicht den Wechselkurs', html: 'Der feste Kurs bleibt bei 166,386 Peseten pro Euro. Nur der CPI-Vergleich ändert sich mit dem Ursprungsjahr und beschreibt einen durchschnittlichen Warenkorb.' },
|
|
52
|
+
],
|
|
53
|
+
faq, bibliography, howTo, schemas,
|
|
54
|
+
};
|