@jjlmoya/utils-finance 1.16.0 → 1.18.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.
Files changed (54) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +3 -1
  3. package/src/entries.ts +7 -1
  4. package/src/tests/locale_completeness.test.ts +1 -1
  5. package/src/tests/tool_validation.test.ts +2 -2
  6. package/src/tool/financialFreedom/bibliography.astro +14 -0
  7. package/src/tool/financialFreedom/bibliography.ts +12 -0
  8. package/src/tool/financialFreedom/component.astro +253 -0
  9. package/src/tool/financialFreedom/entry.ts +27 -0
  10. package/src/tool/financialFreedom/financial-freedom-calculator.css +346 -0
  11. package/src/tool/financialFreedom/i18n/de.ts +123 -0
  12. package/src/tool/financialFreedom/i18n/en.ts +182 -0
  13. package/src/tool/financialFreedom/i18n/es.ts +182 -0
  14. package/src/tool/financialFreedom/i18n/fr.ts +123 -0
  15. package/src/tool/financialFreedom/i18n/id.ts +123 -0
  16. package/src/tool/financialFreedom/i18n/it.ts +123 -0
  17. package/src/tool/financialFreedom/i18n/ja.ts +123 -0
  18. package/src/tool/financialFreedom/i18n/ko.ts +123 -0
  19. package/src/tool/financialFreedom/i18n/nl.ts +123 -0
  20. package/src/tool/financialFreedom/i18n/pl.ts +123 -0
  21. package/src/tool/financialFreedom/i18n/pt.ts +123 -0
  22. package/src/tool/financialFreedom/i18n/ru.ts +123 -0
  23. package/src/tool/financialFreedom/i18n/sv.ts +123 -0
  24. package/src/tool/financialFreedom/i18n/tr.ts +123 -0
  25. package/src/tool/financialFreedom/i18n/zh.ts +123 -0
  26. package/src/tool/financialFreedom/index.ts +10 -0
  27. package/src/tool/financialFreedom/logic.ts +68 -0
  28. package/src/tool/financialFreedom/seo.astro +13 -0
  29. package/src/tool/financialFreedom/ui.ts +26 -0
  30. package/src/tool/profitability/bibliography.astro +14 -0
  31. package/src/tool/profitability/bibliography.ts +12 -0
  32. package/src/tool/profitability/component.astro +249 -0
  33. package/src/tool/profitability/entry.ts +27 -0
  34. package/src/tool/profitability/i18n/de.ts +116 -0
  35. package/src/tool/profitability/i18n/en.ts +165 -0
  36. package/src/tool/profitability/i18n/es.ts +152 -0
  37. package/src/tool/profitability/i18n/fr.ts +112 -0
  38. package/src/tool/profitability/i18n/id.ts +112 -0
  39. package/src/tool/profitability/i18n/it.ts +112 -0
  40. package/src/tool/profitability/i18n/ja.ts +112 -0
  41. package/src/tool/profitability/i18n/ko.ts +112 -0
  42. package/src/tool/profitability/i18n/nl.ts +112 -0
  43. package/src/tool/profitability/i18n/pl.ts +112 -0
  44. package/src/tool/profitability/i18n/pt.ts +112 -0
  45. package/src/tool/profitability/i18n/ru.ts +112 -0
  46. package/src/tool/profitability/i18n/sv.ts +112 -0
  47. package/src/tool/profitability/i18n/tr.ts +112 -0
  48. package/src/tool/profitability/i18n/zh.ts +112 -0
  49. package/src/tool/profitability/index.ts +10 -0
  50. package/src/tool/profitability/logic.ts +43 -0
  51. package/src/tool/profitability/profitability-calculator.css +263 -0
  52. package/src/tool/profitability/seo.astro +15 -0
  53. package/src/tool/profitability/ui.ts +20 -0
  54. package/src/tools.ts +3 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-finance",
3
- "version": "1.16.0",
3
+ "version": "1.18.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -1,5 +1,7 @@
1
1
  import type { FinanceCategoryEntry } from '../types';
2
2
  import { compoundInterest } from '../tool/compoundInterest/entry';
3
+ import { financialFreedom } from '../tool/financialFreedom/entry';
4
+ import { profitability } from '../tool/profitability/entry';
3
5
  import { mortgage } from '../tool/mortgage/entry';
4
6
  import { inflation } from '../tool/inflation/entry';
5
7
  import { percentageCalculator } from '../tool/percentageCalculator/entry';
@@ -14,7 +16,7 @@ import { debtSnowball } from '../tool/debtSnowball/entry';
14
16
 
15
17
  export const financeCategory: FinanceCategoryEntry = {
16
18
  icon: 'mdi:finance',
17
- tools: [compoundInterest, mortgage, inflation, percentageCalculator, lateInterest, ibanBicSwift, rentIncrease, lotteryOptimizer, courtFeeCalculator, legalInterestRate, fireCalculator, debtSnowball],
19
+ tools: [compoundInterest, financialFreedom, profitability, mortgage, inflation, percentageCalculator, lateInterest, ibanBicSwift, rentIncrease, lotteryOptimizer, courtFeeCalculator, legalInterestRate, fireCalculator, debtSnowball],
18
20
  i18n: {
19
21
  en: () => import('./i18n/en').then((m) => m.content),
20
22
  es: () => import('./i18n/es').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  export { compoundInterest } from './tool/compoundInterest/entry';
2
2
  export type { CompoundInterestLocaleContent } from './tool/compoundInterest/entry';
3
+ export { financialFreedom } from './tool/financialFreedom/entry';
4
+ export type { FinancialFreedomLocaleContent } from './tool/financialFreedom/entry';
5
+ export { profitability } from './tool/profitability/entry';
6
+ export type { ProfitabilityLocaleContent } from './tool/profitability/entry';
3
7
  export { courtFeeCalculator } from './tool/courtFeeCalculator/entry';
4
8
  export type { CourtFeeCalculatorLocaleContent } from './tool/courtFeeCalculator/entry';
5
9
  export { debtSnowball } from './tool/debtSnowball/entry';
@@ -24,6 +28,8 @@ export { rentIncrease } from './tool/rentIncreaseCalculator/entry';
24
28
  export type { RentIncreaseLocaleContent } from './tool/rentIncreaseCalculator/entry';
25
29
  export { financeCategory } from './category';
26
30
  import { compoundInterest } from './tool/compoundInterest/entry';
31
+ import { financialFreedom } from './tool/financialFreedom/entry';
32
+ import { profitability } from './tool/profitability/entry';
27
33
  import { courtFeeCalculator } from './tool/courtFeeCalculator/entry';
28
34
  import { debtSnowball } from './tool/debtSnowball/entry';
29
35
  import { fireCalculator } from './tool/fireCalculator/entry';
@@ -35,4 +41,4 @@ import { lotteryOptimizer } from './tool/lotteryOptimizer/entry';
35
41
  import { mortgage } from './tool/mortgage/entry';
36
42
  import { percentageCalculator } from './tool/percentageCalculator/entry';
37
43
  import { rentIncrease } from './tool/rentIncreaseCalculator/entry';
38
- export const ALL_ENTRIES = [compoundInterest, courtFeeCalculator, debtSnowball, fireCalculator, ibanBicSwift, inflation, lateInterest, legalInterestRate, lotteryOptimizer, mortgage, percentageCalculator, rentIncrease];
44
+ export const ALL_ENTRIES = [compoundInterest, financialFreedom, profitability, courtFeeCalculator, debtSnowball, fireCalculator, ibanBicSwift, inflation, lateInterest, legalInterestRate, lotteryOptimizer, mortgage, percentageCalculator, rentIncrease];
@@ -26,7 +26,7 @@ describe('Locale Completeness Validation', () => {
26
26
  });
27
27
 
28
28
  it('all 12 tools registered', () => {
29
- expect(ALL_TOOLS.length).toBe(12);
29
+ expect(ALL_TOOLS.length).toBe(14);
30
30
  });
31
31
  });
32
32
 
@@ -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 12 tools in ALL_TOOLS (compoundInterest, mortgage, inflation, percentageCalculator, lateInterest, ibanBicSwift, rentIncrease, lotteryOptimizer, courtFeeCalculator, legalInterestRate, fireCalculator, debtSnowball)', () => {
8
- expect(ALL_TOOLS.length).toBe(12);
7
+ it('should have 14 tools in ALL_TOOLS (compoundInterest, financialFreedom, profitability, mortgage, inflation, percentageCalculator, lateInterest, ibanBicSwift, rentIncrease, lotteryOptimizer, courtFeeCalculator, legalInterestRate, fireCalculator, debtSnowball)', () => {
8
+ expect(ALL_TOOLS.length).toBe(14);
9
9
  });
10
10
 
11
11
  it('financeCategory should be defined', () => {
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { financialFreedom } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const content = await financialFreedom.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,12 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Financial Independence, Retire Early (FIRE) - Wikipedia',
6
+ url: 'https://en.wikipedia.org/wiki/FIRE_movement',
7
+ },
8
+ {
9
+ name: 'The Trinity Study: Retirement Savings and Withdrawal Rates',
10
+ url: 'https://en.wikipedia.org/wiki/Trinity_study',
11
+ }
12
+ ];
@@ -0,0 +1,253 @@
1
+ ---
2
+ import type { KnownLocale } from '../../types';
3
+ import type { FinancialFreedomUI } from './ui';
4
+
5
+ interface Props {
6
+ locale?: KnownLocale;
7
+ ui?: Record<string, unknown>;
8
+ }
9
+
10
+ const { ui } = Astro.props;
11
+ const t = (ui ?? {}) as FinancialFreedomUI;
12
+ ---
13
+
14
+ <div
15
+ class="ff-root"
16
+ data-label-years={t.labelYears}
17
+ data-label-months={t.labelMonths}
18
+ data-label-days={t.labelDays}
19
+ data-label-years-short={t.labelYearsShort}
20
+ data-label-months-short={t.labelMonthsShort}
21
+ data-label-days-short={t.labelDaysShort}
22
+ data-label-red-zone={t.labelRedZone}
23
+ data-label-yellow-zone={t.labelYellowZone}
24
+ data-label-green-zone={t.labelGreenZone}
25
+ data-locale={Astro.props.locale || 'en'}
26
+ >
27
+ <div class="ff-card">
28
+ <div class="ff-calculator-grid">
29
+ <div class="ff-inputs">
30
+ <div class="ff-input-group">
31
+ <div class="ff-input-label">
32
+ <span>{t.labelSavings}</span>
33
+ <select id="ff-currency-select" class="ff-currency-select">
34
+ <option value="USD">USD ($)</option>
35
+ <option value="EUR">EUR (€)</option>
36
+ <option value="GBP">GBP (£)</option>
37
+ <option value="JPY">JPY (¥)</option>
38
+ <option value="CNY">CNY (元)</option>
39
+ <option value="RUB">RUB (₽)</option>
40
+ <option value="KRW">KRW (₩)</option>
41
+ <option value="TRY">TRY (₺)</option>
42
+ <option value="PLN">PLN (zł)</option>
43
+ <option value="SEK">SEK (kr)</option>
44
+ <option value="IDR">IDR (Rp)</option>
45
+ </select>
46
+ </div>
47
+ <input type="number" id="ff-savings-input" value="50000" class="ff-input-field" />
48
+ <input type="range" id="ff-savings-slider" min="0" max="1000000" step="1000" value="50000" class="ff-slider" />
49
+ </div>
50
+
51
+ <div class="ff-input-group">
52
+ <div class="ff-input-label">
53
+ <span>{t.labelExpenses}</span>
54
+ <div class="ff-switch">
55
+ <button class="ff-switch-btn active" data-period="monthly">{t.labelMonthly}</button>
56
+ <button class="ff-switch-btn" data-period="yearly">{t.labelYearly}</button>
57
+ </div>
58
+ </div>
59
+ <input type="number" id="ff-expenses-input" value="2000" class="ff-input-field" />
60
+ <input type="range" id="ff-expenses-slider" min="0" max="20000" step="100" value="2000" class="ff-slider" />
61
+ </div>
62
+ </div>
63
+
64
+ <div class="ff-results-main">
65
+ <div class="ff-time-hero">
66
+ <div class="ff-time-label">{t.labelTimeRemaining}</div>
67
+ <div id="ff-time-display" class="ff-time-display">0</div>
68
+ </div>
69
+
70
+ <div class="ff-time-units">
71
+ <div class="ff-unit-card">
72
+ <span id="ff-years" class="ff-unit-val">0</span>
73
+ <span class="ff-unit-name">{t.labelYears}</span>
74
+ </div>
75
+ <div class="ff-unit-card">
76
+ <span id="ff-months" class="ff-unit-val">0</span>
77
+ <span class="ff-unit-name">{t.labelMonths}</span>
78
+ </div>
79
+ <div class="ff-unit-card">
80
+ <span id="ff-days" class="ff-unit-val">0</span>
81
+ <span class="ff-unit-name">{t.labelDays}</span>
82
+ </div>
83
+ </div>
84
+
85
+ <div id="ff-status" class="ff-status-indicator status-red">
86
+ <span id="ff-status-text">{t.labelRedZone}</span>
87
+ </div>
88
+
89
+ <div class="ff-burn-grid">
90
+ <div class="ff-unit-card ff-burn-card">
91
+ <div class="ff-unit-name">{t.labelBurnRateDaily}</div>
92
+ <div id="ff-burn-daily" class="ff-burn-val">0</div>
93
+ </div>
94
+ <div class="ff-unit-card ff-burn-card">
95
+ <div class="ff-unit-name">{t.labelBurnRateMonthly}</div>
96
+ <div id="ff-burn-monthly" class="ff-burn-val">0</div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ </div>
101
+
102
+ <div class="ff-what-if-card">
103
+ <div class="ff-what-if-title">{t.labelWhatIf}</div>
104
+ <p class="ff-what-if-desc">{t.labelWhatIfDescription}</p>
105
+ <div class="ff-gain-display">
106
+ <span id="ff-gain-time">0</span> {t.labelGainTime}
107
+ </div>
108
+ </div>
109
+ </div>
110
+ </div>
111
+
112
+ <script>
113
+ import { FinancialFreedomCalculator } from './logic';
114
+ import type { FinancialFreedomResult } from './logic';
115
+
116
+ const root = document.querySelector('.ff-root') as HTMLElement;
117
+ const savingsSlider = document.getElementById('ff-savings-slider') as HTMLInputElement;
118
+ const savingsInput = document.getElementById('ff-savings-input') as HTMLInputElement;
119
+ const expensesSlider = document.getElementById('ff-expenses-slider') as HTMLInputElement;
120
+ const expensesInput = document.getElementById('ff-expenses-input') as HTMLInputElement;
121
+ const currencySelect = document.getElementById('ff-currency-select') as HTMLSelectElement;
122
+ const periodBtns = document.querySelectorAll('.ff-switch-btn');
123
+
124
+ const yearsOut = document.getElementById('ff-years');
125
+ const monthsOut = document.getElementById('ff-months');
126
+ const daysOut = document.getElementById('ff-days');
127
+ const burnDailyOut = document.getElementById('ff-burn-daily');
128
+ const burnMonthlyOut = document.getElementById('ff-burn-monthly');
129
+ const gainTimeOut = document.getElementById('ff-gain-time');
130
+ const statusOut = document.getElementById('ff-status');
131
+ const statusTextOut = document.getElementById('ff-status-text');
132
+ const timeHeroOut = document.getElementById('ff-time-display');
133
+
134
+ const labelRedZone = root.dataset.labelRedZone || '';
135
+ const labelYellowZone = root.dataset.labelYellowZone || '';
136
+ const labelGreenZone = root.dataset.labelGreenZone || '';
137
+
138
+ let currentPeriod: 'monthly' | 'yearly' = 'monthly';
139
+
140
+ const labelYearsShort = root.dataset.labelYearsShort || 'y';
141
+ const labelMonthsShort = root.dataset.labelMonthsShort || 'm';
142
+ const labelDaysShort = root.dataset.labelDaysShort || 'd';
143
+
144
+ const formatHumanTimeShort = (res: { years: number; months: number; days: number }) => {
145
+ if (res.years > 0) return `${res.years}${labelYearsShort} ${res.months}${labelMonthsShort}`;
146
+ if (res.months > 0) return `${res.months}${labelMonthsShort} ${res.days}${labelDaysShort}`;
147
+ return `${res.days}${labelDaysShort}`;
148
+ };
149
+
150
+ const setText = (el: HTMLElement | null, text: string) => {
151
+ if (el) el.innerText = text;
152
+ };
153
+
154
+ const updateDisplay = (result: FinancialFreedomResult, currencyCode: string) => {
155
+ const locale = root.dataset.locale || 'en';
156
+ const fmt = (max: number) => new Intl.NumberFormat(locale, {
157
+ style: 'currency', currency: currencyCode, maximumFractionDigits: max
158
+ });
159
+
160
+ const { years, months, days } = result.humanTime;
161
+ setText(yearsOut, years.toString());
162
+ setText(monthsOut, months.toString());
163
+ setText(daysOut, days.toString());
164
+
165
+ setText(timeHeroOut, formatHumanTimeShort(result.humanTime));
166
+ setText(burnDailyOut, fmt(2).format(result.dailyBurn));
167
+ setText(burnMonthlyOut, fmt(0).format(result.monthlyBurn));
168
+
169
+ if (gainTimeOut) {
170
+ const g = result.gainWithSavings;
171
+ gainTimeOut.innerText = `${g.years}${labelYearsShort}, ${g.months}${labelMonthsShort}, ${g.days}${labelDaysShort}`;
172
+ }
173
+ };
174
+
175
+ const updateStatus = (status: string) => {
176
+ if (!statusOut || !statusTextOut) return;
177
+ statusOut.className = `ff-status-indicator status-${status}`;
178
+ if (status === 'red') statusTextOut.innerText = labelRedZone;
179
+ if (status === 'yellow') statusTextOut.innerText = labelYellowZone;
180
+ if (status === 'green') statusTextOut.innerText = labelGreenZone;
181
+ };
182
+
183
+ const update = () => {
184
+ const savings = parseFloat(savingsInput.value) || 0;
185
+ const expenses = parseFloat(expensesInput.value) || 0;
186
+ const currencyCode = currencySelect.value;
187
+
188
+ const result = FinancialFreedomCalculator.calculate({
189
+ savings,
190
+ expenses,
191
+ expensePeriod: currentPeriod
192
+ });
193
+
194
+ updateDisplay(result, currencyCode);
195
+ updateStatus(result.status);
196
+ };
197
+
198
+ const syncInput = (slider: HTMLInputElement, input: HTMLInputElement) => {
199
+ slider.addEventListener('input', () => {
200
+ input.value = slider.value;
201
+ update();
202
+ });
203
+ input.addEventListener('input', () => {
204
+ slider.value = input.value;
205
+ update();
206
+ });
207
+ };
208
+
209
+ syncInput(savingsSlider, savingsInput);
210
+ syncInput(expensesSlider, expensesInput);
211
+
212
+ currencySelect.addEventListener('change', update);
213
+
214
+ const handlePeriodChange = (btn: HTMLElement) => {
215
+ periodBtns.forEach(b => b.classList.remove('active'));
216
+ btn.classList.add('active');
217
+ currentPeriod = btn.dataset.period as 'monthly' | 'yearly';
218
+
219
+ if (currentPeriod === 'monthly') {
220
+ expensesSlider.max = '20000';
221
+ expensesSlider.step = '100';
222
+ if (parseFloat(expensesInput.value) > 20000) expensesInput.value = '20000';
223
+ } else {
224
+ expensesSlider.max = '240000';
225
+ expensesSlider.step = '1000';
226
+ }
227
+ expensesSlider.value = expensesInput.value;
228
+
229
+ update();
230
+ };
231
+
232
+ const preselectCurrency = () => {
233
+ const locale = root.dataset.locale || 'en';
234
+ const mapping: Record<string, string> = {
235
+ es: 'EUR', de: 'EUR', fr: 'EUR', it: 'EUR', pt: 'EUR', nl: 'EUR',
236
+ en: 'USD',
237
+ tr: 'TRY', pl: 'PLN', sv: 'SEK', id: 'IDR',
238
+ ru: 'RUB', ja: 'JPY', zh: 'CNY', ko: 'KRW'
239
+ };
240
+ if (mapping[locale]) currencySelect.value = mapping[locale];
241
+ };
242
+
243
+ periodBtns.forEach(btn => {
244
+ btn.addEventListener('click', () => handlePeriodChange(btn as HTMLElement));
245
+ });
246
+
247
+ preselectCurrency();
248
+ update();
249
+ </script>
250
+
251
+ <style>
252
+ @import './financial-freedom-calculator.css';
253
+ </style>
@@ -0,0 +1,27 @@
1
+ import type { FinanceToolEntry } from '../../types';
2
+ import type { FinancialFreedomUI } from './ui';
3
+
4
+ export const financialFreedom: FinanceToolEntry<FinancialFreedomUI> = {
5
+ id: 'financial-freedom-calculator',
6
+ icons: {
7
+ bg: 'mdi:bank-outline',
8
+ fg: 'mdi:clock-fast',
9
+ },
10
+ i18n: {
11
+ en: () => import('./i18n/en').then((m) => m.content),
12
+ es: () => import('./i18n/es').then((m) => m.content),
13
+ fr: () => import('./i18n/fr').then((m) => m.content),
14
+ de: () => import('./i18n/de').then((m) => m.content),
15
+ it: () => import('./i18n/it').then((m) => m.content),
16
+ pt: () => import('./i18n/pt').then((m) => m.content),
17
+ nl: () => import('./i18n/nl').then((m) => m.content),
18
+ id: () => import('./i18n/id').then((m) => m.content),
19
+ pl: () => import('./i18n/pl').then((m) => m.content),
20
+ tr: () => import('./i18n/tr').then((m) => m.content),
21
+ ru: () => import('./i18n/ru').then((m) => m.content),
22
+ sv: () => import('./i18n/sv').then((m) => m.content),
23
+ ja: () => import('./i18n/ja').then((m) => m.content),
24
+ ko: () => import('./i18n/ko').then((m) => m.content),
25
+ zh: () => import('./i18n/zh').then((m) => m.content),
26
+ },
27
+ };