@jjlmoya/utils-finance 1.1.0 → 1.3.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
CHANGED
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
---
|
|
2
|
-
import { Icon } from
|
|
3
|
-
import type { KnownLocale } from '../../types';
|
|
2
|
+
import { Icon } from "astro-icon/components";
|
|
4
3
|
|
|
5
4
|
interface Props {
|
|
6
|
-
|
|
7
|
-
ui?: Record<string, unknown>;
|
|
5
|
+
ui?: Record<string, unknown>;
|
|
8
6
|
}
|
|
9
7
|
|
|
10
8
|
const { ui } = Astro.props;
|
|
11
9
|
---
|
|
12
10
|
|
|
13
|
-
<div
|
|
11
|
+
<div
|
|
12
|
+
class="snow-container"
|
|
13
|
+
id="debt-calc-container"
|
|
14
|
+
data-currency-symbol={ui.currencySymbol}
|
|
15
|
+
data-currency-code={ui.currencyCode}
|
|
16
|
+
data-currency-locale={ui.currencyLocale}
|
|
17
|
+
data-clear-confirm={ui.labelClearDataConfirm}
|
|
18
|
+
data-label-debt-infinite={ui.labelDebtInfinite}
|
|
19
|
+
data-label-priority={ui.labelPriority}
|
|
20
|
+
data-label-step={ui.labelStep}
|
|
21
|
+
data-label-monthly-payment={ui.labelMonthlyPayment}
|
|
22
|
+
data-label-free={ui.labelFree}
|
|
23
|
+
data-label-active-method={ui.labelActiveMethod}
|
|
24
|
+
data-label-date={ui.labelDate}
|
|
25
|
+
data-label-starting-balance={ui.labelStartingBalance}
|
|
26
|
+
data-label-interest={ui.labelInterest}
|
|
27
|
+
data-label-amortization={ui.labelAmortization}
|
|
28
|
+
data-label-total-month={ui.labelTotalMonth}
|
|
29
|
+
data-label-remaining={ui.labelRemaining}
|
|
30
|
+
data-label-delete-debt={ui.labelDeleteDebt}
|
|
31
|
+
data-label-snowball-method={ui.labelSnowballMethod}
|
|
32
|
+
data-label-avalanche-method={ui.labelAvalancheMethod}
|
|
33
|
+
>
|
|
14
34
|
<div class="snow-main-card">
|
|
15
35
|
<aside class="snow-sidebar">
|
|
16
36
|
<h2 class="snow-title">
|
|
@@ -19,46 +39,94 @@ const { ui } = Astro.props;
|
|
|
19
39
|
</h2>
|
|
20
40
|
|
|
21
41
|
<div class="snow-input-group">
|
|
22
|
-
<label class="snow-label" for="extra-budget"
|
|
42
|
+
<label class="snow-label" for="extra-budget"
|
|
43
|
+
>{ui.labelExtraBudget}</label
|
|
44
|
+
>
|
|
23
45
|
<div class="snow-input-wrapper">
|
|
24
|
-
<input
|
|
46
|
+
<input
|
|
47
|
+
type="number"
|
|
48
|
+
id="extra-budget"
|
|
49
|
+
class="snow-input"
|
|
50
|
+
value="100"
|
|
51
|
+
min="0"
|
|
52
|
+
step="50"
|
|
53
|
+
/>
|
|
25
54
|
<span class="snow-input-unit">{ui.currencySymbol}</span>
|
|
26
55
|
</div>
|
|
27
56
|
<p class="snow-help-text">{ui.labelExtraBudgetHelp}</p>
|
|
28
57
|
</div>
|
|
29
58
|
|
|
30
59
|
<div class="snow-method-tabs" id="method-switcher">
|
|
31
|
-
<button class="snow-tab active" data-method="snowball"
|
|
32
|
-
|
|
60
|
+
<button class="snow-tab active" data-method="snowball"
|
|
61
|
+
>{ui.labelSnowballMethod}</button
|
|
62
|
+
>
|
|
63
|
+
<button class="snow-tab" data-method="avalanche"
|
|
64
|
+
>{ui.labelAvalancheMethod}</button
|
|
65
|
+
>
|
|
33
66
|
</div>
|
|
34
67
|
|
|
35
68
|
<div class="snow-adder-card">
|
|
36
69
|
<span class="snow-section-label">{ui.labelAddNewDebt}</span>
|
|
37
70
|
<div class="snow-adder-content">
|
|
38
71
|
<div class="snow-input-group">
|
|
39
|
-
<input
|
|
72
|
+
<input
|
|
73
|
+
type="text"
|
|
74
|
+
id="debt-name"
|
|
75
|
+
class="snow-input"
|
|
76
|
+
placeholder={ui.labelDebtNamePlaceholder}
|
|
77
|
+
/>
|
|
40
78
|
</div>
|
|
41
79
|
<div class="snow-grid-2">
|
|
42
80
|
<div class="snow-input-group">
|
|
43
|
-
<label class="snow-label" for="debt-balance"
|
|
81
|
+
<label class="snow-label" for="debt-balance"
|
|
82
|
+
>{ui.labelDebtBalance}</label
|
|
83
|
+
>
|
|
44
84
|
<div class="snow-input-wrapper">
|
|
45
|
-
<input
|
|
46
|
-
|
|
85
|
+
<input
|
|
86
|
+
type="number"
|
|
87
|
+
id="debt-balance"
|
|
88
|
+
class="snow-input"
|
|
89
|
+
placeholder="0"
|
|
90
|
+
min="0"
|
|
91
|
+
/>
|
|
92
|
+
<span class="snow-input-unit"
|
|
93
|
+
>{ui.currencySymbol}</span
|
|
94
|
+
>
|
|
47
95
|
</div>
|
|
48
96
|
</div>
|
|
49
97
|
<div class="snow-input-group">
|
|
50
|
-
<label class="snow-label" for="debt-min-payment"
|
|
98
|
+
<label class="snow-label" for="debt-min-payment"
|
|
99
|
+
>{ui.labelMinPayment}</label
|
|
100
|
+
>
|
|
51
101
|
<div class="snow-input-wrapper">
|
|
52
|
-
<input
|
|
53
|
-
|
|
102
|
+
<input
|
|
103
|
+
type="number"
|
|
104
|
+
id="debt-min-payment"
|
|
105
|
+
class="snow-input"
|
|
106
|
+
placeholder="0"
|
|
107
|
+
min="0"
|
|
108
|
+
/>
|
|
109
|
+
<span class="snow-input-unit"
|
|
110
|
+
>{ui.currencySymbol}/mes</span
|
|
111
|
+
>
|
|
54
112
|
</div>
|
|
55
113
|
</div>
|
|
56
114
|
</div>
|
|
57
115
|
<div class="snow-input-group">
|
|
58
|
-
<label class="snow-label" for="debt-rate"
|
|
116
|
+
<label class="snow-label" for="debt-rate"
|
|
117
|
+
>{ui.labelInterestRate}</label
|
|
118
|
+
>
|
|
59
119
|
<div class="snow-input-wrapper">
|
|
60
|
-
<input
|
|
61
|
-
|
|
120
|
+
<input
|
|
121
|
+
type="number"
|
|
122
|
+
id="debt-rate"
|
|
123
|
+
class="snow-input"
|
|
124
|
+
value="15"
|
|
125
|
+
step="0.1"
|
|
126
|
+
/>
|
|
127
|
+
<span class="snow-input-unit"
|
|
128
|
+
>% {ui.labelInterestHelp}</span
|
|
129
|
+
>
|
|
62
130
|
</div>
|
|
63
131
|
</div>
|
|
64
132
|
<p class="snow-input-help">{ui.labelAddNewDebt}</p>
|
|
@@ -81,12 +149,16 @@ const { ui } = Astro.props;
|
|
|
81
149
|
<Icon name="mdi:bank-off-outline" size={48} />
|
|
82
150
|
</div>
|
|
83
151
|
<h3 class="snow-empty-title">{ui.labelEmptyStateTitle}</h3>
|
|
84
|
-
<p class="snow-empty-description">
|
|
152
|
+
<p class="snow-empty-description">
|
|
153
|
+
{ui.labelEmptyStateDescription}
|
|
154
|
+
</p>
|
|
85
155
|
</div>
|
|
86
156
|
|
|
87
157
|
<div id="results-content" class="snow-hidden">
|
|
88
158
|
<div class="snow-inventory-area">
|
|
89
|
-
<span class="snow-section-label"
|
|
159
|
+
<span class="snow-section-label"
|
|
160
|
+
>{ui.labelDebtInventory}</span
|
|
161
|
+
>
|
|
90
162
|
<div class="snow-mini-table">
|
|
91
163
|
<div class="snow-table-row snow-table-header">
|
|
92
164
|
<span>{ui.labelDebtName}</span>
|
|
@@ -101,46 +173,70 @@ const { ui } = Astro.props;
|
|
|
101
173
|
|
|
102
174
|
<div class="snow-results-hero">
|
|
103
175
|
<div class="snow-hero-main">
|
|
104
|
-
<span class="snow-hero-label"
|
|
105
|
-
|
|
176
|
+
<span class="snow-hero-label"
|
|
177
|
+
>{ui.labelFreedomDate}</span
|
|
178
|
+
>
|
|
179
|
+
<div class="snow-hero-value" id="summary-date">
|
|
180
|
+
ENERO 2026
|
|
181
|
+
</div>
|
|
106
182
|
</div>
|
|
107
183
|
<div class="snow-hero-badge">
|
|
108
184
|
<Icon name="mdi:check-decagram" size={20} />
|
|
109
|
-
<span id="active-method-label"
|
|
185
|
+
<span id="active-method-label"
|
|
186
|
+
>{ui.labelSnowballMethod}</span
|
|
187
|
+
>
|
|
110
188
|
</div>
|
|
111
189
|
</div>
|
|
112
190
|
|
|
113
191
|
<div class="snow-grid-stats">
|
|
114
192
|
<div class="snow-stat-box">
|
|
115
193
|
<div class="snow-stat-value" id="stat-months">--</div>
|
|
116
|
-
<div class="snow-stat-label">
|
|
194
|
+
<div class="snow-stat-label">
|
|
195
|
+
{ui.labelMonthsRemaining}
|
|
196
|
+
</div>
|
|
117
197
|
</div>
|
|
118
198
|
<div class="snow-stat-box">
|
|
119
199
|
<div class="snow-stat-value" id="stat-interest">--</div>
|
|
120
|
-
<div class="snow-stat-label">
|
|
200
|
+
<div class="snow-stat-label">
|
|
201
|
+
{ui.labelTotalInterest}
|
|
202
|
+
</div>
|
|
121
203
|
</div>
|
|
122
204
|
<div class="snow-stat-box">
|
|
123
|
-
<div class="snow-stat-value" id="stat-total-paid"
|
|
205
|
+
<div class="snow-stat-value" id="stat-total-paid">
|
|
206
|
+
--
|
|
207
|
+
</div>
|
|
124
208
|
<div class="snow-stat-label">{ui.labelTotalPaid}</div>
|
|
125
209
|
</div>
|
|
126
210
|
<div class="snow-stat-box snow-stat-highlight">
|
|
127
|
-
<div class="snow-stat-value" id="stat-extra-saved"
|
|
128
|
-
|
|
211
|
+
<div class="snow-stat-value" id="stat-extra-saved">
|
|
212
|
+
--
|
|
213
|
+
</div>
|
|
214
|
+
<div class="snow-stat-label">
|
|
215
|
+
{ui.labelInterestSaved}
|
|
216
|
+
</div>
|
|
129
217
|
</div>
|
|
130
218
|
</div>
|
|
131
219
|
|
|
132
220
|
<div class="snow-action-plan">
|
|
133
221
|
<div class="snow-action-header">
|
|
134
|
-
<h4 class="snow-action-title">
|
|
135
|
-
|
|
222
|
+
<h4 class="snow-action-title">
|
|
223
|
+
{ui.labelYourStrategy}
|
|
224
|
+
</h4>
|
|
225
|
+
<div class="snow-action-help">
|
|
226
|
+
{ui.labelStrategyHelp}
|
|
227
|
+
</div>
|
|
136
228
|
</div>
|
|
137
229
|
<div id="action-plan-list" class="snow-action-list"></div>
|
|
138
230
|
</div>
|
|
139
231
|
|
|
140
232
|
<div class="snow-table-container">
|
|
141
233
|
<div class="snow-table-header-action">
|
|
142
|
-
<h4 class="snow-table-title">
|
|
143
|
-
|
|
234
|
+
<h4 class="snow-table-title">
|
|
235
|
+
{ui.labelDetailedAmortization}
|
|
236
|
+
</h4>
|
|
237
|
+
<button id="export-csv" class="snow-btn-export"
|
|
238
|
+
>{ui.labelExportCSV}</button
|
|
239
|
+
>
|
|
144
240
|
</div>
|
|
145
241
|
<div class="snow-table-overlay">
|
|
146
242
|
<div class="snow-table-scroll">
|
|
@@ -180,7 +276,7 @@ const { ui } = Astro.props;
|
|
|
180
276
|
}
|
|
181
277
|
|
|
182
278
|
:global(.theme-dark),
|
|
183
|
-
:global([data-theme=
|
|
279
|
+
:global([data-theme="dark"]) {
|
|
184
280
|
--snow-bg-card: rgba(15, 23, 42, 0.9);
|
|
185
281
|
--snow-border: rgba(30, 41, 59, 1);
|
|
186
282
|
--snow-text-main: #f8fafc;
|
|
@@ -323,7 +419,7 @@ const { ui } = Astro.props;
|
|
|
323
419
|
}
|
|
324
420
|
|
|
325
421
|
:global(.theme-dark) .snow-tab.active,
|
|
326
|
-
:global([data-theme=
|
|
422
|
+
:global([data-theme="dark"]) .snow-tab.active {
|
|
327
423
|
background: rgba(255, 255, 255, 0.1);
|
|
328
424
|
color: white;
|
|
329
425
|
}
|
|
@@ -365,7 +461,11 @@ const { ui } = Astro.props;
|
|
|
365
461
|
}
|
|
366
462
|
|
|
367
463
|
.snow-btn-add {
|
|
368
|
-
background: linear-gradient(
|
|
464
|
+
background: linear-gradient(
|
|
465
|
+
135deg,
|
|
466
|
+
var(--snow-emerald),
|
|
467
|
+
var(--snow-blue)
|
|
468
|
+
);
|
|
369
469
|
color: white;
|
|
370
470
|
}
|
|
371
471
|
|
|
@@ -467,7 +567,7 @@ const { ui } = Astro.props;
|
|
|
467
567
|
overflow-y: auto;
|
|
468
568
|
}
|
|
469
569
|
|
|
470
|
-
.snow-debt-item {
|
|
570
|
+
:global(.snow-debt-item) {
|
|
471
571
|
display: grid;
|
|
472
572
|
grid-template-columns: 1fr 1fr 1fr 1fr 60px;
|
|
473
573
|
padding: 1rem;
|
|
@@ -476,7 +576,7 @@ const { ui } = Astro.props;
|
|
|
476
576
|
gap: 1rem;
|
|
477
577
|
}
|
|
478
578
|
|
|
479
|
-
.snow-debt-remove {
|
|
579
|
+
:global(.snow-debt-remove) {
|
|
480
580
|
background: none;
|
|
481
581
|
border: none;
|
|
482
582
|
color: var(--snow-rose);
|
|
@@ -487,12 +587,16 @@ const { ui } = Astro.props;
|
|
|
487
587
|
transition: opacity 0.3s ease;
|
|
488
588
|
}
|
|
489
589
|
|
|
490
|
-
.snow-debt-remove:hover {
|
|
590
|
+
:global(.snow-debt-remove:hover) {
|
|
491
591
|
opacity: 1;
|
|
492
592
|
}
|
|
493
593
|
|
|
494
594
|
.snow-results-hero {
|
|
495
|
-
background: linear-gradient(
|
|
595
|
+
background: linear-gradient(
|
|
596
|
+
135deg,
|
|
597
|
+
rgba(16, 185, 129, 0.1),
|
|
598
|
+
rgba(59, 130, 246, 0.1)
|
|
599
|
+
);
|
|
496
600
|
border: 1.5px solid var(--snow-border);
|
|
497
601
|
border-radius: 1.75rem;
|
|
498
602
|
padding: 3rem;
|
|
@@ -609,7 +713,7 @@ const { ui } = Astro.props;
|
|
|
609
713
|
gap: 1rem;
|
|
610
714
|
}
|
|
611
715
|
|
|
612
|
-
.snow-plan-item {
|
|
716
|
+
:global(.snow-plan-item) {
|
|
613
717
|
display: grid;
|
|
614
718
|
grid-template-columns: 50px 1fr 1fr;
|
|
615
719
|
gap: 1.5rem;
|
|
@@ -620,12 +724,12 @@ const { ui } = Astro.props;
|
|
|
620
724
|
border-radius: 1rem;
|
|
621
725
|
}
|
|
622
726
|
|
|
623
|
-
.snow-plan-item.active-step {
|
|
727
|
+
:global(.snow-plan-item.active-step) {
|
|
624
728
|
background: rgba(16, 185, 129, 0.05);
|
|
625
729
|
border-color: rgba(16, 185, 129, 0.3);
|
|
626
730
|
}
|
|
627
731
|
|
|
628
|
-
.snow-step-marker {
|
|
732
|
+
:global(.snow-step-marker) {
|
|
629
733
|
width: 50px;
|
|
630
734
|
height: 50px;
|
|
631
735
|
background: var(--snow-emerald);
|
|
@@ -638,13 +742,13 @@ const { ui } = Astro.props;
|
|
|
638
742
|
font-size: 1.25rem;
|
|
639
743
|
}
|
|
640
744
|
|
|
641
|
-
.snow-step-body {
|
|
745
|
+
:global(.snow-step-body) {
|
|
642
746
|
display: flex;
|
|
643
747
|
flex-direction: column;
|
|
644
748
|
gap: 0.25rem;
|
|
645
749
|
}
|
|
646
750
|
|
|
647
|
-
.snow-step-label {
|
|
751
|
+
:global(.snow-step-label) {
|
|
648
752
|
font-size: 0.65rem;
|
|
649
753
|
font-weight: 900;
|
|
650
754
|
text-transform: uppercase;
|
|
@@ -652,13 +756,13 @@ const { ui } = Astro.props;
|
|
|
652
756
|
opacity: 0.6;
|
|
653
757
|
}
|
|
654
758
|
|
|
655
|
-
.snow-step-name {
|
|
759
|
+
:global(.snow-step-name) {
|
|
656
760
|
font-size: 1rem;
|
|
657
761
|
font-weight: 900;
|
|
658
762
|
color: var(--snow-text-main);
|
|
659
763
|
}
|
|
660
764
|
|
|
661
|
-
.snow-step-amount {
|
|
765
|
+
:global(.snow-step-amount) {
|
|
662
766
|
display: flex;
|
|
663
767
|
flex-direction: column;
|
|
664
768
|
align-items: flex-end;
|
|
@@ -666,13 +770,13 @@ const { ui } = Astro.props;
|
|
|
666
770
|
text-align: right;
|
|
667
771
|
}
|
|
668
772
|
|
|
669
|
-
.snow-step-amount span {
|
|
773
|
+
:global(.snow-step-amount span) {
|
|
670
774
|
font-size: 0.65rem;
|
|
671
775
|
opacity: 0.6;
|
|
672
776
|
text-transform: uppercase;
|
|
673
777
|
}
|
|
674
778
|
|
|
675
|
-
.snow-step-amount strong {
|
|
779
|
+
:global(.snow-step-amount strong) {
|
|
676
780
|
font-size: 1.25rem;
|
|
677
781
|
font-weight: 900;
|
|
678
782
|
color: var(--snow-emerald);
|
|
@@ -746,21 +850,21 @@ const { ui } = Astro.props;
|
|
|
746
850
|
border-bottom: 1px solid var(--snow-border);
|
|
747
851
|
}
|
|
748
852
|
|
|
749
|
-
table td {
|
|
853
|
+
:global(table td) {
|
|
750
854
|
padding: 1rem;
|
|
751
855
|
border-bottom: 1px solid var(--snow-border);
|
|
752
856
|
}
|
|
753
857
|
|
|
754
|
-
table tr:last-child td {
|
|
858
|
+
:global(table tr:last-child td) {
|
|
755
859
|
border-bottom: none;
|
|
756
860
|
}
|
|
757
861
|
|
|
758
|
-
.snow-table-free {
|
|
862
|
+
:global(.snow-table-free) {
|
|
759
863
|
color: var(--snow-emerald);
|
|
760
864
|
font-weight: 900;
|
|
761
865
|
}
|
|
762
866
|
|
|
763
|
-
.snow-table-muted {
|
|
867
|
+
:global(.snow-table-muted) {
|
|
764
868
|
opacity: 0.6;
|
|
765
869
|
}
|
|
766
870
|
|
|
@@ -785,7 +889,7 @@ const { ui } = Astro.props;
|
|
|
785
889
|
.snow-table-header {
|
|
786
890
|
display: none;
|
|
787
891
|
}
|
|
788
|
-
.snow-debt-item {
|
|
892
|
+
:global(.snow-debt-item) {
|
|
789
893
|
grid-template-columns: 1fr;
|
|
790
894
|
padding: 1rem 0;
|
|
791
895
|
border-left: 4px solid var(--snow-emerald);
|
|
@@ -795,18 +899,56 @@ const { ui } = Astro.props;
|
|
|
795
899
|
</style>
|
|
796
900
|
|
|
797
901
|
<script>
|
|
798
|
-
import {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
902
|
+
import {
|
|
903
|
+
DebtSnowballLogic,
|
|
904
|
+
type Debt,
|
|
905
|
+
type SimulationResult,
|
|
906
|
+
} from "./logic";
|
|
907
|
+
import { format, addMonths } from "date-fns";
|
|
908
|
+
import { es, enUS, fr, type Locale } from "date-fns/locale";
|
|
909
|
+
|
|
910
|
+
const container = document.getElementById("debt-calc-container");
|
|
911
|
+
const currencyLocale =
|
|
912
|
+
container?.getAttribute("data-currency-locale") || "es-ES";
|
|
913
|
+
const currencyCode = container?.getAttribute("data-currency-code") || "EUR";
|
|
914
|
+
const clearConfirm =
|
|
915
|
+
container?.getAttribute("data-clear-confirm") || "¿Vaciar plan?";
|
|
916
|
+
const labelDebtInfinite =
|
|
917
|
+
container?.getAttribute("data-label-debt-infinite") || "DEUDA INFINITA";
|
|
918
|
+
const labelPriority =
|
|
919
|
+
container?.getAttribute("data-label-priority") || "PRIORIDAD MÁXIMA";
|
|
920
|
+
const labelStep = container?.getAttribute("data-label-step") || "PASO";
|
|
921
|
+
const labelMonthlyPayment =
|
|
922
|
+
container?.getAttribute("data-label-monthly-payment") || "Pago mensual";
|
|
923
|
+
const labelFree = container?.getAttribute("data-label-free") || "LIBRE";
|
|
924
|
+
const labelActiveMethod =
|
|
925
|
+
container?.getAttribute("data-label-active-method") || "MÉTODO";
|
|
926
|
+
const labelDate = container?.getAttribute("data-label-date") || "Fecha";
|
|
927
|
+
const labelStartingBalance =
|
|
928
|
+
container?.getAttribute("data-label-starting-balance") ||
|
|
929
|
+
"Saldo Inicial";
|
|
930
|
+
const labelInterestCsv =
|
|
931
|
+
container?.getAttribute("data-label-interest") || "Interes";
|
|
932
|
+
const labelAmortization =
|
|
933
|
+
container?.getAttribute("data-label-amortization") || "Amortizacion";
|
|
934
|
+
const labelTotalMonth =
|
|
935
|
+
container?.getAttribute("data-label-total-month") || "Pago Total";
|
|
936
|
+
const labelRemaining =
|
|
937
|
+
container?.getAttribute("data-label-remaining") || "Saldo Final";
|
|
938
|
+
const labelDeleteDebt =
|
|
939
|
+
container?.getAttribute("data-label-delete-debt") || "Eliminar";
|
|
940
|
+
const labelSnowballMethod =
|
|
941
|
+
container?.getAttribute("data-label-snowball-method") || "SNOWBALL";
|
|
942
|
+
const labelAvalancheMethod =
|
|
943
|
+
container?.getAttribute("data-label-avalanche-method") || "AVALANCHE";
|
|
944
|
+
|
|
945
|
+
const localeKey = document.documentElement.lang || "es";
|
|
946
|
+
const dateLocales: Record<string, Locale> = { es, en: enUS, fr };
|
|
947
|
+
const dateLocale = dateLocales[localeKey] ?? es;
|
|
806
948
|
|
|
807
949
|
function formatCurrency(val: number): string {
|
|
808
950
|
return new Intl.NumberFormat(currencyLocale, {
|
|
809
|
-
style:
|
|
951
|
+
style: "currency",
|
|
810
952
|
currency: currencyCode,
|
|
811
953
|
maximumFractionDigits: 0,
|
|
812
954
|
}).format(val);
|
|
@@ -815,7 +957,7 @@ const { ui } = Astro.props;
|
|
|
815
957
|
class SnowballEngine {
|
|
816
958
|
private debts: Debt[] = [];
|
|
817
959
|
private extra: number = 100;
|
|
818
|
-
private method:
|
|
960
|
+
private method: "snowball" | "avalanche" = "snowball";
|
|
819
961
|
|
|
820
962
|
constructor() {
|
|
821
963
|
this.load();
|
|
@@ -826,42 +968,49 @@ const { ui } = Astro.props;
|
|
|
826
968
|
private bind() {
|
|
827
969
|
const get = (id: string) => document.getElementById(id);
|
|
828
970
|
|
|
829
|
-
get(
|
|
971
|
+
get("add-debt-btn")?.addEventListener("click", () => this.add());
|
|
830
972
|
|
|
831
|
-
const extraInput = get(
|
|
832
|
-
extraInput?.addEventListener(
|
|
973
|
+
const extraInput = get("extra-budget") as HTMLInputElement;
|
|
974
|
+
extraInput?.addEventListener("input", (e) => {
|
|
833
975
|
const val = parseFloat((e.target as HTMLInputElement).value);
|
|
834
976
|
this.extra = isNaN(val) ? 0 : val;
|
|
835
977
|
this.save();
|
|
836
978
|
});
|
|
837
979
|
|
|
838
|
-
document.querySelectorAll(
|
|
839
|
-
tab.addEventListener(
|
|
840
|
-
document
|
|
841
|
-
|
|
842
|
-
|
|
980
|
+
document.querySelectorAll(".snow-tab").forEach((tab) => {
|
|
981
|
+
tab.addEventListener("click", () => {
|
|
982
|
+
document
|
|
983
|
+
.querySelectorAll(".snow-tab")
|
|
984
|
+
.forEach((t) => t.classList.remove("active"));
|
|
985
|
+
tab.classList.add("active");
|
|
986
|
+
this.method =
|
|
987
|
+
((tab as HTMLElement).dataset.method as
|
|
988
|
+
| "snowball"
|
|
989
|
+
| "avalanche") || "snowball";
|
|
843
990
|
this.render();
|
|
844
991
|
});
|
|
845
992
|
});
|
|
846
993
|
|
|
847
|
-
get(
|
|
994
|
+
get("clear-data")?.addEventListener("click", () => {
|
|
848
995
|
if (confirm(clearConfirm)) {
|
|
849
996
|
this.debts = [];
|
|
850
997
|
this.save();
|
|
851
998
|
}
|
|
852
999
|
});
|
|
853
1000
|
|
|
854
|
-
get(
|
|
1001
|
+
get("export-csv")?.addEventListener("click", () => this.export());
|
|
855
1002
|
}
|
|
856
1003
|
|
|
857
1004
|
private load() {
|
|
858
1005
|
try {
|
|
859
|
-
const d = localStorage.getItem(
|
|
1006
|
+
const d = localStorage.getItem("snow_debts_v2");
|
|
860
1007
|
if (d) this.debts = JSON.parse(d);
|
|
861
|
-
const e = localStorage.getItem(
|
|
1008
|
+
const e = localStorage.getItem("snow_extra_v2");
|
|
862
1009
|
if (e) {
|
|
863
1010
|
this.extra = parseFloat(e);
|
|
864
|
-
const input = document.getElementById(
|
|
1011
|
+
const input = document.getElementById(
|
|
1012
|
+
"extra-budget",
|
|
1013
|
+
) as HTMLInputElement;
|
|
865
1014
|
if (input) input.value = e;
|
|
866
1015
|
}
|
|
867
1016
|
} catch {
|
|
@@ -870,29 +1019,52 @@ const { ui } = Astro.props;
|
|
|
870
1019
|
}
|
|
871
1020
|
|
|
872
1021
|
private save() {
|
|
873
|
-
localStorage.setItem(
|
|
874
|
-
localStorage.setItem(
|
|
1022
|
+
localStorage.setItem("snow_debts_v2", JSON.stringify(this.debts));
|
|
1023
|
+
localStorage.setItem("snow_extra_v2", this.extra.toString());
|
|
875
1024
|
this.render();
|
|
876
1025
|
}
|
|
877
1026
|
|
|
878
1027
|
private getDebtInputs() {
|
|
879
1028
|
return {
|
|
880
|
-
nameEl: document.getElementById(
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
1029
|
+
nameEl: document.getElementById(
|
|
1030
|
+
"debt-name",
|
|
1031
|
+
) as HTMLInputElement,
|
|
1032
|
+
balEl: document.getElementById(
|
|
1033
|
+
"debt-balance",
|
|
1034
|
+
) as HTMLInputElement,
|
|
1035
|
+
minEl: document.getElementById(
|
|
1036
|
+
"debt-min-payment",
|
|
1037
|
+
) as HTMLInputElement,
|
|
1038
|
+
rateEl: document.getElementById(
|
|
1039
|
+
"debt-rate",
|
|
1040
|
+
) as HTMLInputElement,
|
|
884
1041
|
};
|
|
885
1042
|
}
|
|
886
1043
|
|
|
887
|
-
private clearDebtInputs(
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1044
|
+
private clearDebtInputs(
|
|
1045
|
+
nameEl: HTMLInputElement,
|
|
1046
|
+
balEl: HTMLInputElement,
|
|
1047
|
+
minEl: HTMLInputElement,
|
|
1048
|
+
) {
|
|
1049
|
+
balEl.value = "";
|
|
1050
|
+
minEl.value = "";
|
|
1051
|
+
nameEl.value = "";
|
|
891
1052
|
}
|
|
892
1053
|
|
|
893
|
-
private validateAndCreateDebt(
|
|
1054
|
+
private validateAndCreateDebt(
|
|
1055
|
+
b: number,
|
|
1056
|
+
m: number,
|
|
1057
|
+
name: string,
|
|
1058
|
+
rate: number,
|
|
1059
|
+
): boolean {
|
|
894
1060
|
if (b > 0 && m > 0) {
|
|
895
|
-
this.debts.push({
|
|
1061
|
+
this.debts.push({
|
|
1062
|
+
id: Math.random().toString(36).substring(2, 11),
|
|
1063
|
+
name,
|
|
1064
|
+
balance: b,
|
|
1065
|
+
minPayment: m,
|
|
1066
|
+
rate,
|
|
1067
|
+
});
|
|
896
1068
|
return true;
|
|
897
1069
|
}
|
|
898
1070
|
return false;
|
|
@@ -904,7 +1076,7 @@ const { ui } = Astro.props;
|
|
|
904
1076
|
|
|
905
1077
|
const b = parseFloat(balEl.value);
|
|
906
1078
|
const m = parseFloat(minEl.value);
|
|
907
|
-
const name = nameEl?.value || `
|
|
1079
|
+
const name = nameEl?.value || `D${this.debts.length + 1}`;
|
|
908
1080
|
const rate = parseFloat(rateEl?.value) || 0;
|
|
909
1081
|
|
|
910
1082
|
if (this.validateAndCreateDebt(b, m, name, rate)) {
|
|
@@ -914,24 +1086,36 @@ const { ui } = Astro.props;
|
|
|
914
1086
|
}
|
|
915
1087
|
|
|
916
1088
|
private render() {
|
|
917
|
-
const snow = DebtSnowballLogic.simulate(
|
|
918
|
-
|
|
919
|
-
|
|
1089
|
+
const snow = DebtSnowballLogic.simulate(
|
|
1090
|
+
this.debts,
|
|
1091
|
+
this.extra,
|
|
1092
|
+
"snowball",
|
|
1093
|
+
);
|
|
1094
|
+
const aval = DebtSnowballLogic.simulate(
|
|
1095
|
+
this.debts,
|
|
1096
|
+
this.extra,
|
|
1097
|
+
"avalanche",
|
|
1098
|
+
);
|
|
1099
|
+
const res = this.method === "snowball" ? snow : aval;
|
|
920
1100
|
this.renderView(res, snow, aval);
|
|
921
1101
|
}
|
|
922
1102
|
|
|
923
|
-
private renderView(
|
|
924
|
-
|
|
925
|
-
|
|
1103
|
+
private renderView(
|
|
1104
|
+
res: SimulationResult | null,
|
|
1105
|
+
snow: SimulationResult | null,
|
|
1106
|
+
aval: SimulationResult | null,
|
|
1107
|
+
) {
|
|
1108
|
+
const noMsg = document.getElementById("no-debts-msg");
|
|
1109
|
+
const results = document.getElementById("results-content");
|
|
926
1110
|
|
|
927
1111
|
if (!res || !snow || !aval) {
|
|
928
|
-
noMsg?.classList.remove(
|
|
929
|
-
results?.classList.add(
|
|
1112
|
+
noMsg?.classList.remove("snow-hidden");
|
|
1113
|
+
results?.classList.add("snow-hidden");
|
|
930
1114
|
return;
|
|
931
1115
|
}
|
|
932
1116
|
|
|
933
|
-
noMsg?.classList.add(
|
|
934
|
-
results?.classList.remove(
|
|
1117
|
+
noMsg?.classList.add("snow-hidden");
|
|
1118
|
+
results?.classList.remove("snow-hidden");
|
|
935
1119
|
this.updateDisplay(res);
|
|
936
1120
|
this.updateDebtList();
|
|
937
1121
|
this.updateActionPlan();
|
|
@@ -939,109 +1123,134 @@ const { ui } = Astro.props;
|
|
|
939
1123
|
}
|
|
940
1124
|
|
|
941
1125
|
private updateDisplay(res: SimulationResult) {
|
|
942
|
-
const dateEl = document.getElementById(
|
|
1126
|
+
const dateEl = document.getElementById("summary-date");
|
|
943
1127
|
if (dateEl) {
|
|
944
1128
|
if (res.isGrowing) {
|
|
945
|
-
dateEl.textContent =
|
|
946
|
-
dateEl.style.color =
|
|
1129
|
+
dateEl.textContent = labelDebtInfinite;
|
|
1130
|
+
dateEl.style.color = "var(--snow-rose)";
|
|
947
1131
|
} else {
|
|
948
|
-
dateEl.textContent = format(
|
|
949
|
-
|
|
1132
|
+
dateEl.textContent = format(
|
|
1133
|
+
addMonths(new Date(), res.mo),
|
|
1134
|
+
"MMMM yyyy",
|
|
1135
|
+
{ locale: dateLocale },
|
|
1136
|
+
).toUpperCase();
|
|
1137
|
+
dateEl.style.color = "var(--snow-emerald)";
|
|
950
1138
|
}
|
|
951
1139
|
}
|
|
952
1140
|
|
|
953
|
-
const methEl = document.getElementById(
|
|
954
|
-
if (methEl)
|
|
1141
|
+
const methEl = document.getElementById("active-method-label");
|
|
1142
|
+
if (methEl) {
|
|
1143
|
+
const methodLabel = this.method === 'snowball' ? labelSnowballMethod : labelAvalancheMethod;
|
|
1144
|
+
methEl.textContent = `${labelActiveMethod} ${methodLabel}`;
|
|
1145
|
+
}
|
|
955
1146
|
|
|
956
|
-
document.getElementById(
|
|
957
|
-
|
|
958
|
-
document.getElementById(
|
|
1147
|
+
document.getElementById("stat-months")!.textContent =
|
|
1148
|
+
res.mo.toString();
|
|
1149
|
+
document.getElementById("stat-interest")!.textContent =
|
|
1150
|
+
formatCurrency(res.totalI);
|
|
1151
|
+
document.getElementById("stat-total-paid")!.textContent =
|
|
1152
|
+
formatCurrency(res.totalP);
|
|
959
1153
|
|
|
960
1154
|
const baseSim = DebtSnowballLogic.simulateNoExtra(this.debts);
|
|
961
1155
|
const saved = baseSim ? baseSim.totalI - res.totalI : 0;
|
|
962
|
-
document.getElementById(
|
|
1156
|
+
document.getElementById("stat-extra-saved")!.textContent =
|
|
1157
|
+
formatCurrency(Math.max(0, saved));
|
|
963
1158
|
}
|
|
964
1159
|
|
|
965
1160
|
private updateDebtList() {
|
|
966
|
-
const list = document.getElementById(
|
|
1161
|
+
const list = document.getElementById("debt-items-list");
|
|
967
1162
|
if (!list) return;
|
|
968
|
-
list.innerHTML =
|
|
1163
|
+
list.innerHTML = "";
|
|
969
1164
|
this.debts.forEach((d) => {
|
|
970
|
-
const row = document.createElement(
|
|
971
|
-
row.className =
|
|
1165
|
+
const row = document.createElement("div");
|
|
1166
|
+
row.className = "snow-debt-item";
|
|
972
1167
|
row.innerHTML = `
|
|
973
1168
|
<span>${d.name}</span>
|
|
974
1169
|
<span>${formatCurrency(d.balance)}</span>
|
|
975
1170
|
<span>${formatCurrency(d.minPayment)}</span>
|
|
976
1171
|
<span>${d.rate}%</span>
|
|
977
|
-
<button class="snow-debt-remove" title={
|
|
1172
|
+
<button class="snow-debt-remove" title="${labelDeleteDebt}">×</button>
|
|
978
1173
|
`;
|
|
979
|
-
row.querySelector(
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1174
|
+
row.querySelector(".snow-debt-remove")?.addEventListener(
|
|
1175
|
+
"click",
|
|
1176
|
+
() => {
|
|
1177
|
+
this.debts = this.debts.filter(
|
|
1178
|
+
(item) => item.id !== d.id,
|
|
1179
|
+
);
|
|
1180
|
+
this.save();
|
|
1181
|
+
},
|
|
1182
|
+
);
|
|
983
1183
|
list.appendChild(row);
|
|
984
1184
|
});
|
|
985
1185
|
}
|
|
986
1186
|
|
|
987
1187
|
private updateActionPlan() {
|
|
988
|
-
const planList = document.getElementById(
|
|
1188
|
+
const planList = document.getElementById("action-plan-list");
|
|
989
1189
|
if (!planList) return;
|
|
990
1190
|
const ordered = [...this.debts].sort((a, b) =>
|
|
991
|
-
this.method ===
|
|
1191
|
+
this.method === "snowball"
|
|
1192
|
+
? a.balance - b.balance
|
|
1193
|
+
: b.rate - a.rate,
|
|
992
1194
|
);
|
|
993
1195
|
let cumulativeExtra = this.extra;
|
|
994
1196
|
planList.innerHTML = ordered
|
|
995
1197
|
.map((d, index) => {
|
|
996
1198
|
const totalForThis = d.minPayment + cumulativeExtra;
|
|
997
1199
|
cumulativeExtra += d.minPayment;
|
|
998
|
-
const stepLabel =
|
|
1200
|
+
const stepLabel =
|
|
1201
|
+
index === 0
|
|
1202
|
+
? labelPriority
|
|
1203
|
+
: `${labelStep} ${index + 1}`;
|
|
999
1204
|
return `
|
|
1000
|
-
<div class="snow-plan-item ${index === 0 ?
|
|
1205
|
+
<div class="snow-plan-item ${index === 0 ? "active-step" : ""}">
|
|
1001
1206
|
<div class="snow-step-marker">${index + 1}</div>
|
|
1002
1207
|
<div class="snow-step-body">
|
|
1003
1208
|
<span class="snow-step-label">${stepLabel}</span>
|
|
1004
1209
|
<div class="snow-step-name">${d.name}</div>
|
|
1005
1210
|
</div>
|
|
1006
1211
|
<div class="snow-step-amount">
|
|
1007
|
-
<span
|
|
1212
|
+
<span>${labelMonthlyPayment}</span>
|
|
1008
1213
|
<strong>${formatCurrency(totalForThis)}</strong>
|
|
1009
1214
|
</div>
|
|
1010
1215
|
</div>
|
|
1011
1216
|
`;
|
|
1012
1217
|
})
|
|
1013
|
-
.join(
|
|
1218
|
+
.join("");
|
|
1014
1219
|
}
|
|
1015
1220
|
|
|
1016
1221
|
private updateProjectionTable(res: SimulationResult) {
|
|
1017
|
-
const tbody = document.querySelector(
|
|
1222
|
+
const tbody = document.querySelector("#projection-table tbody");
|
|
1018
1223
|
if (!tbody) return;
|
|
1019
1224
|
tbody.innerHTML = res.proj
|
|
1020
1225
|
.map(
|
|
1021
1226
|
(p) => `
|
|
1022
1227
|
<tr>
|
|
1023
|
-
<td>${format(addMonths(new Date(), p.mo),
|
|
1228
|
+
<td>${format(addMonths(new Date(), p.mo), "MMM yy", { locale: dateLocale }).toUpperCase()}</td>
|
|
1024
1229
|
<td>${formatCurrency(p.iniB)}</td>
|
|
1025
1230
|
<td>${formatCurrency(p.int)}</td>
|
|
1026
1231
|
<td>${formatCurrency(p.princ)}</td>
|
|
1027
1232
|
<td>${formatCurrency(p.total)}</td>
|
|
1028
|
-
<td class="${p.finB < 1 ?
|
|
1233
|
+
<td class="${p.finB < 1 ? "snow-table-free" : "snow-table-muted"}">${p.finB < 1 ? labelFree : formatCurrency(p.finB)}</td>
|
|
1029
1234
|
</tr>
|
|
1030
|
-
|
|
1235
|
+
`,
|
|
1031
1236
|
)
|
|
1032
|
-
.join(
|
|
1237
|
+
.join("");
|
|
1033
1238
|
}
|
|
1034
1239
|
|
|
1035
1240
|
private export() {
|
|
1036
|
-
const res = DebtSnowballLogic.simulate(
|
|
1241
|
+
const res = DebtSnowballLogic.simulate(
|
|
1242
|
+
this.debts,
|
|
1243
|
+
this.extra,
|
|
1244
|
+
this.method,
|
|
1245
|
+
);
|
|
1037
1246
|
if (!res) return;
|
|
1038
|
-
let csv =
|
|
1247
|
+
let csv = `\uFEFF${labelDate},${labelStartingBalance},${labelInterestCsv},${labelAmortization},${labelTotalMonth},${labelRemaining}\n`;
|
|
1039
1248
|
res.proj.forEach((p) => {
|
|
1040
|
-
const d = format(addMonths(new Date(), p.mo),
|
|
1249
|
+
const d = format(addMonths(new Date(), p.mo), "MM/yyyy");
|
|
1041
1250
|
csv += `${d},${p.iniB.toFixed(2)},${p.int.toFixed(2)},${p.princ.toFixed(2)},${p.total.toFixed(2)},${p.finB.toFixed(2)}\n`;
|
|
1042
1251
|
});
|
|
1043
|
-
const blob = new Blob([csv], { type:
|
|
1044
|
-
const link = document.createElement(
|
|
1252
|
+
const blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
|
|
1253
|
+
const link = document.createElement("a");
|
|
1045
1254
|
link.href = URL.createObjectURL(blob);
|
|
1046
1255
|
link.download = `plan_deuda_${this.method}.csv`;
|
|
1047
1256
|
link.click();
|
|
@@ -11,7 +11,7 @@ const { ui } = Astro.props;
|
|
|
11
11
|
const availableYears = InflationCalculator.getAvailableYears();
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
<div class="inflation-container" data-currency-locale={ui.currencyLocale} data-currency-code={ui.currencyCode}>
|
|
14
|
+
<div class="inflation-container" data-currency-locale={ui.currencyLocale} data-currency-code={ui.currencyCode} data-label-inflation-accumulated={ui.labelInflationAccumulated}>
|
|
15
15
|
<div class="inflation-header">
|
|
16
16
|
<div class="inflation-header-title-group">
|
|
17
17
|
<div class="inflation-header-icon">
|
|
@@ -78,6 +78,7 @@ const availableYears = InflationCalculator.getAvailableYears();
|
|
|
78
78
|
const container = document.querySelector('.inflation-container') as HTMLElement;
|
|
79
79
|
const currencyLocale = container.dataset.currencyLocale || 'es-ES';
|
|
80
80
|
const currencyCode = container.dataset.currencyCode || 'EUR';
|
|
81
|
+
const labelInflationAccumulated = container.dataset.labelInflationAccumulated || 'Inflación Acumulada';
|
|
81
82
|
|
|
82
83
|
const amountEl = document.getElementById('inflation-amount') as HTMLInputElement;
|
|
83
84
|
const yearEl = document.getElementById('inflation-year') as HTMLSelectElement;
|
|
@@ -107,7 +108,7 @@ const availableYears = InflationCalculator.getAvailableYears();
|
|
|
107
108
|
const sign = result.percentChange > 0 ? '+' : '';
|
|
108
109
|
const span = percentEl.querySelector('span');
|
|
109
110
|
if (span) {
|
|
110
|
-
span.innerText = `${sign}${result.percentChange} %
|
|
111
|
+
span.innerText = `${sign}${result.percentChange} % ${labelInflationAccumulated}`;
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
114
|
}
|
|
@@ -2,7 +2,7 @@ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dt
|
|
|
2
2
|
import type { ToolLocaleContent } from '../../../types';
|
|
3
3
|
import type { LateInterestUI } from '../ui';
|
|
4
4
|
|
|
5
|
-
const slug = 'intereses-demora';
|
|
5
|
+
const slug = 'calculadora-intereses-demora';
|
|
6
6
|
const title = 'Calculadora de Intereses de Demora y Legales España 2026';
|
|
7
7
|
const description =
|
|
8
8
|
'Calcula intereses de demora tributarios (Hacienda) y legales por impagos. Simulador actualizado para 2025 y 2026 basado en la Ley de Presupuestos.';
|
|
@@ -2,7 +2,7 @@ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dt
|
|
|
2
2
|
import type { ToolLocaleContent } from '../../../types';
|
|
3
3
|
import type { MortgageUI } from '../ui';
|
|
4
4
|
|
|
5
|
-
const slug = '
|
|
5
|
+
const slug = 'hipoteca';
|
|
6
6
|
const title = 'Simulador de Hipoteca y Tabla de Amortización Francesa';
|
|
7
7
|
const description =
|
|
8
8
|
'Calcula tu cuota hipotecaria mensual, descubre cuánto pagarás en intereses y visualiza tu cuadro de amortización completo al instante.';
|