@jjlmoya/utils-finance 1.1.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 (133) hide show
  1. package/package.json +64 -0
  2. package/src/category/i18n/en.ts +159 -0
  3. package/src/category/i18n/es.ts +159 -0
  4. package/src/category/i18n/fr.ts +159 -0
  5. package/src/category/index.ts +24 -0
  6. package/src/category/seo.astro +15 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +11 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +32 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +19 -0
  17. package/src/tests/locale_completeness.test.ts +39 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/no_h1_in_components.test.ts +48 -0
  20. package/src/tests/schemas_fulfillment.test.ts +23 -0
  21. package/src/tests/seo_length.test.ts +23 -0
  22. package/src/tests/title_quality.test.ts +55 -0
  23. package/src/tests/tool_validation.test.ts +17 -0
  24. package/src/tool/compoundInterest/bibliography.astro +14 -0
  25. package/src/tool/compoundInterest/component.astro +697 -0
  26. package/src/tool/compoundInterest/i18n/en.ts +212 -0
  27. package/src/tool/compoundInterest/i18n/es.ts +212 -0
  28. package/src/tool/compoundInterest/i18n/fr.ts +212 -0
  29. package/src/tool/compoundInterest/index.ts +34 -0
  30. package/src/tool/compoundInterest/logic.ts +40 -0
  31. package/src/tool/compoundInterest/seo.astro +14 -0
  32. package/src/tool/compoundInterest/ui.ts +19 -0
  33. package/src/tool/courtFeeCalculator/bibliography.astro +14 -0
  34. package/src/tool/courtFeeCalculator/component.astro +427 -0
  35. package/src/tool/courtFeeCalculator/i18n/en.ts +268 -0
  36. package/src/tool/courtFeeCalculator/i18n/es.ts +268 -0
  37. package/src/tool/courtFeeCalculator/i18n/fr.ts +268 -0
  38. package/src/tool/courtFeeCalculator/index.ts +33 -0
  39. package/src/tool/courtFeeCalculator/logic.ts +86 -0
  40. package/src/tool/courtFeeCalculator/seo.astro +14 -0
  41. package/src/tool/courtFeeCalculator/ui.ts +31 -0
  42. package/src/tool/debtSnowball/bibliography.astro +14 -0
  43. package/src/tool/debtSnowball/component.astro +1052 -0
  44. package/src/tool/debtSnowball/i18n/en.ts +288 -0
  45. package/src/tool/debtSnowball/i18n/es.ts +288 -0
  46. package/src/tool/debtSnowball/i18n/fr.ts +288 -0
  47. package/src/tool/debtSnowball/index.ts +37 -0
  48. package/src/tool/debtSnowball/logic.ts +156 -0
  49. package/src/tool/debtSnowball/seo.astro +14 -0
  50. package/src/tool/debtSnowball/ui.ts +49 -0
  51. package/src/tool/fireCalculator/bibliography.astro +14 -0
  52. package/src/tool/fireCalculator/component.astro +689 -0
  53. package/src/tool/fireCalculator/i18n/en.ts +261 -0
  54. package/src/tool/fireCalculator/i18n/es.ts +261 -0
  55. package/src/tool/fireCalculator/i18n/fr.ts +261 -0
  56. package/src/tool/fireCalculator/index.ts +33 -0
  57. package/src/tool/fireCalculator/logic.ts +108 -0
  58. package/src/tool/fireCalculator/seo.astro +14 -0
  59. package/src/tool/fireCalculator/ui.ts +33 -0
  60. package/src/tool/ibanBicSwiftConverter/bibliography.astro +14 -0
  61. package/src/tool/ibanBicSwiftConverter/component.astro +581 -0
  62. package/src/tool/ibanBicSwiftConverter/i18n/en.ts +167 -0
  63. package/src/tool/ibanBicSwiftConverter/i18n/es.ts +216 -0
  64. package/src/tool/ibanBicSwiftConverter/i18n/fr.ts +167 -0
  65. package/src/tool/ibanBicSwiftConverter/index.ts +33 -0
  66. package/src/tool/ibanBicSwiftConverter/logic.ts +101 -0
  67. package/src/tool/ibanBicSwiftConverter/seo.astro +14 -0
  68. package/src/tool/ibanBicSwiftConverter/ui.ts +17 -0
  69. package/src/tool/inflation/bibliography.astro +14 -0
  70. package/src/tool/inflation/component.astro +400 -0
  71. package/src/tool/inflation/i18n/en.ts +301 -0
  72. package/src/tool/inflation/i18n/es.ts +301 -0
  73. package/src/tool/inflation/i18n/fr.ts +301 -0
  74. package/src/tool/inflation/index.ts +34 -0
  75. package/src/tool/inflation/logic.ts +92 -0
  76. package/src/tool/inflation/seo.astro +14 -0
  77. package/src/tool/inflation/ui.ts +17 -0
  78. package/src/tool/lateInterest/bibliography.astro +14 -0
  79. package/src/tool/lateInterest/component.astro +655 -0
  80. package/src/tool/lateInterest/i18n/en.ts +255 -0
  81. package/src/tool/lateInterest/i18n/es.ts +224 -0
  82. package/src/tool/lateInterest/i18n/fr.ts +224 -0
  83. package/src/tool/lateInterest/index.ts +34 -0
  84. package/src/tool/lateInterest/logic.ts +89 -0
  85. package/src/tool/lateInterest/seo.astro +14 -0
  86. package/src/tool/lateInterest/ui.ts +33 -0
  87. package/src/tool/legalInterestRate/bibliography.astro +14 -0
  88. package/src/tool/legalInterestRate/component.astro +650 -0
  89. package/src/tool/legalInterestRate/i18n/en.ts +159 -0
  90. package/src/tool/legalInterestRate/i18n/es.ts +159 -0
  91. package/src/tool/legalInterestRate/i18n/fr.ts +159 -0
  92. package/src/tool/legalInterestRate/index.ts +33 -0
  93. package/src/tool/legalInterestRate/logic.ts +29 -0
  94. package/src/tool/legalInterestRate/seo.astro +14 -0
  95. package/src/tool/legalInterestRate/ui.ts +23 -0
  96. package/src/tool/lotteryOptimizer/bibliography.astro +14 -0
  97. package/src/tool/lotteryOptimizer/component.astro +863 -0
  98. package/src/tool/lotteryOptimizer/i18n/en.ts +188 -0
  99. package/src/tool/lotteryOptimizer/i18n/es.ts +281 -0
  100. package/src/tool/lotteryOptimizer/i18n/fr.ts +188 -0
  101. package/src/tool/lotteryOptimizer/index.ts +33 -0
  102. package/src/tool/lotteryOptimizer/logic.ts +133 -0
  103. package/src/tool/lotteryOptimizer/seo.astro +14 -0
  104. package/src/tool/lotteryOptimizer/ui.ts +33 -0
  105. package/src/tool/mortgage/bibliography.astro +14 -0
  106. package/src/tool/mortgage/component.astro +1030 -0
  107. package/src/tool/mortgage/i18n/en.ts +227 -0
  108. package/src/tool/mortgage/i18n/es.ts +227 -0
  109. package/src/tool/mortgage/i18n/fr.ts +227 -0
  110. package/src/tool/mortgage/index.ts +34 -0
  111. package/src/tool/mortgage/logic.ts +118 -0
  112. package/src/tool/mortgage/seo.astro +14 -0
  113. package/src/tool/mortgage/ui.ts +35 -0
  114. package/src/tool/percentageCalculator/bibliography.astro +14 -0
  115. package/src/tool/percentageCalculator/component.astro +640 -0
  116. package/src/tool/percentageCalculator/i18n/en.ts +233 -0
  117. package/src/tool/percentageCalculator/i18n/es.ts +233 -0
  118. package/src/tool/percentageCalculator/i18n/fr.ts +233 -0
  119. package/src/tool/percentageCalculator/index.ts +34 -0
  120. package/src/tool/percentageCalculator/logic.ts +48 -0
  121. package/src/tool/percentageCalculator/seo.astro +14 -0
  122. package/src/tool/percentageCalculator/ui.ts +30 -0
  123. package/src/tool/rentIncreaseCalculator/bibliography.astro +14 -0
  124. package/src/tool/rentIncreaseCalculator/component.astro +587 -0
  125. package/src/tool/rentIncreaseCalculator/i18n/en.ts +159 -0
  126. package/src/tool/rentIncreaseCalculator/i18n/es.ts +220 -0
  127. package/src/tool/rentIncreaseCalculator/i18n/fr.ts +159 -0
  128. package/src/tool/rentIncreaseCalculator/index.ts +33 -0
  129. package/src/tool/rentIncreaseCalculator/logic.ts +43 -0
  130. package/src/tool/rentIncreaseCalculator/seo.astro +14 -0
  131. package/src/tool/rentIncreaseCalculator/ui.ts +19 -0
  132. package/src/tools.ts +16 -0
  133. package/src/types.ts +74 -0
@@ -0,0 +1,40 @@
1
+ export interface CompoundInterestInput {
2
+ initialPrincipal: number;
3
+ monthlyContribution: number;
4
+ annualInterestRate: number;
5
+ years: number;
6
+ }
7
+
8
+ export interface YearlyResult {
9
+ year: number;
10
+ invested: number;
11
+ interest: number;
12
+ total: number;
13
+ }
14
+
15
+ export class CompoundInterestCalculator {
16
+ static calculate(input: CompoundInterestInput): YearlyResult[] {
17
+ const { initialPrincipal, monthlyContribution, annualInterestRate, years } = input;
18
+ const monthlyRate = annualInterestRate / 100 / 12;
19
+ const results: YearlyResult[] = [];
20
+
21
+ let currentBalance = initialPrincipal;
22
+ let totalInvested = initialPrincipal;
23
+
24
+ for (let year = 1; year <= years; year++) {
25
+ for (let month = 1; month <= 12; month++) {
26
+ currentBalance = currentBalance * (1 + monthlyRate) + monthlyContribution;
27
+ totalInvested += monthlyContribution;
28
+ }
29
+
30
+ results.push({
31
+ year,
32
+ invested: Number(totalInvested.toFixed(2)),
33
+ interest: Number((currentBalance - totalInvested).toFixed(2)),
34
+ total: Number(currentBalance.toFixed(2)),
35
+ });
36
+ }
37
+
38
+ return results;
39
+ }
40
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { compoundInterest } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const content = await compoundInterest.i18n[locale]?.();
12
+ if (!content) return null;
13
+ ---
14
+ <SEORenderer content={{ locale: locale as string, sections: content.seo }} />
@@ -0,0 +1,19 @@
1
+ export interface CompoundInterestUI extends Record<string, string> {
2
+ labelTitle: string;
3
+ labelRealtime: string;
4
+ labelInitial: string;
5
+ labelMonthly: string;
6
+ labelRate: string;
7
+ labelYears: string;
8
+ labelMyMoney: string;
9
+ labelProfit: string;
10
+ labelTotal: string;
11
+ labelYear: string;
12
+ labelPrincipal: string;
13
+ labelInterest: string;
14
+ insightPrefix: string;
15
+ insightSuffix: string;
16
+ currencySymbol: string;
17
+ currencyCode: string;
18
+ currencyLocale: string;
19
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { courtFeeCalculator } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const content = await courtFeeCalculator.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,427 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ import type { KnownLocale } from '../../types';
4
+
5
+ interface Props {
6
+ locale?: KnownLocale;
7
+ ui?: Record<string, unknown>;
8
+ }
9
+
10
+ const { ui } = Astro.props;
11
+ ---
12
+
13
+ <div class="ctf-container" data-currency-symbol={ui.currencySymbol} data-currency-code={ui.currencyCode} data-currency-locale={ui.currencyLocale}>
14
+ <div class="ctf-wrapper">
15
+ <aside class="ctf-sidebar">
16
+ <div class="ctf-section-title">
17
+ <Icon name="mdi:cog" size={16} />
18
+ {ui.labelLegalParameters}
19
+ </div>
20
+
21
+ <div class="ctf-input-stack">
22
+ <div class="ctf-input-field">
23
+ <label for="entity-type">{ui.labelEntityType}</label>
24
+ <select id="entity-type" class="ctf-input-control">
25
+ <option value="juridica">{ui.labelEntityJuridica}</option>
26
+ <option value="fisica">{ui.labelEntityFisica}</option>
27
+ </select>
28
+ </div>
29
+
30
+ <div class="ctf-input-field">
31
+ <label for="jurisdiction">{ui.labelJurisdiction}</label>
32
+ <select id="jurisdiction" class="ctf-input-control">
33
+ <option value="civil">{ui.labelCivilOrder}</option>
34
+ <option value="contencioso">{ui.labelAdministrativeOrder}</option>
35
+ <option value="social">{ui.labelSocialOrder}</option>
36
+ </select>
37
+ </div>
38
+
39
+ <div class="ctf-input-field">
40
+ <label for="procedure">{ui.labelProcedure}</label>
41
+ <select id="procedure" class="ctf-input-control">
42
+ <option value="ordinario" class="opt-civil">{ui.labelOrdinary} (300€)</option>
43
+ <option value="monitorio" class="opt-civil">{ui.labelVerbal} (100€)</option>
44
+ <option value="ejecutivo" class="opt-civil">{ui.labelExecutive} (200€)</option>
45
+ <option value="apelacion" class="opt-civil">{ui.labelAppeal} (800€)</option>
46
+ <option value="casacion" class="opt-civil">{ui.labelCassation} (1.200€)</option>
47
+ <option value="abreviado" class="opt-contencioso" style="display:none">{ui.labelAbbreviated} (200€)</option>
48
+ <option value="cont-ordinario" class="opt-contencioso" style="display:none">{ui.labelOrdinary} (350€)</option>
49
+ <option value="cont-apelacion" class="opt-contencioso" style="display:none">{ui.labelAppeal} (800€)</option>
50
+ <option value="cont-casacion" class="opt-contencioso" style="display:none">{ui.labelCassation} (1.200€)</option>
51
+ <option value="suplicacion" class="opt-social" style="display:none">{ui.labelSupplication} (500€)</option>
52
+ <option value="soc-casacion" class="opt-social" style="display:none">{ui.labelCassation} (600€)</option>
53
+ </select>
54
+ </div>
55
+
56
+ <div class="ctf-input-field" id="amount-container">
57
+ <label for="claim-amount">{ui.labelClaimAmount}</label>
58
+ <div style="position: relative;">
59
+ <input type="number" id="claim-amount" class="ctf-input-control" value="30000" step="1000" />
60
+ <span style="position: absolute; right: 1rem; top: 50%; transform: translateY(-50%); font-weight: 700; color: var(--ctf-accent);">{ui.currencySymbol}</span>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ </aside>
65
+
66
+ <main class="ctf-main">
67
+ <div class="ctf-result-header">
68
+ <span class="ctf-total-label">{ui.labelEstimatedSettlement}</span>
69
+ <div id="exento-tag" class="ctf-badge-exento" style="display:none">{ui.labelExemptSubject}</div>
70
+ </div>
71
+
72
+ <div class="ctf-total-display">
73
+ <Icon name="mdi:gavel" class="ctf-legal-seal" size={120} />
74
+ <span class="ctf-total-amount" id="val-total">0,00 €</span>
75
+ <span class="ctf-total-label">{ui.labelModel696}</span>
76
+ </div>
77
+
78
+ <div class="ctf-breakdown-list">
79
+ <div class="ctf-breakdown-item">
80
+ <span class="ctf-item-label">{ui.labelFixedQuote}</span>
81
+ <span class="ctf-item-value" id="val-fixed">0,00 €</span>
82
+ </div>
83
+ <div class="ctf-breakdown-item">
84
+ <span class="ctf-item-label">{ui.labelVariableQuote}</span>
85
+ <span class="ctf-item-value" id="val-variable">0,00 €</span>
86
+ </div>
87
+ <div class="ctf-breakdown-item">
88
+ <span class="ctf-item-label">{ui.labelTaxableBase}</span>
89
+ <span class="ctf-item-value" id="val-base">30.000 €</span>
90
+ </div>
91
+ </div>
92
+
93
+ <footer class="ctf-actions-area">
94
+ <button class="ctf-btn-copy" id="btn-copy">
95
+ <Icon name="mdi:content-copy" size={20} />
96
+ {ui.labelCopySettlement}
97
+ </button>
98
+ </footer>
99
+ </main>
100
+ </div>
101
+ </div>
102
+
103
+ <style>
104
+ :root {
105
+ --ctf-bg-card: #fff;
106
+ --ctf-border-card: #e2e8f0;
107
+ --ctf-text-primary: #1e293b;
108
+ --ctf-text-secondary: #64748b;
109
+ --ctf-bg-input: #f8fafc;
110
+ --ctf-border-input: #e2e8f0;
111
+ --ctf-accent: #3b82f6;
112
+ --ctf-accent-hover: #2563eb;
113
+ --ctf-bg-exempt: #dcfce7;
114
+ --ctf-border-exempt: #bbf7d0;
115
+ }
116
+
117
+ :global(.theme-dark) {
118
+ --ctf-bg-card: #1e293b;
119
+ --ctf-border-card: #334155;
120
+ --ctf-text-primary: #f1f5f9;
121
+ --ctf-text-secondary: #cbd5e1;
122
+ --ctf-bg-input: #0f172a;
123
+ --ctf-border-input: #334155;
124
+ --ctf-accent: #60a5fa;
125
+ --ctf-accent-hover: #3b82f6;
126
+ --ctf-bg-exempt: rgba(34, 197, 94, 0.1);
127
+ --ctf-border-exempt: rgba(34, 197, 94, 0.3);
128
+ }
129
+
130
+ :global([data-theme='dark']) {
131
+ --ctf-bg-card: #1e293b;
132
+ --ctf-border-card: #334155;
133
+ --ctf-text-primary: #f1f5f9;
134
+ --ctf-text-secondary: #cbd5e1;
135
+ --ctf-bg-input: #0f172a;
136
+ --ctf-border-input: #334155;
137
+ --ctf-accent: #60a5fa;
138
+ --ctf-accent-hover: #3b82f6;
139
+ --ctf-bg-exempt: rgba(34, 197, 94, 0.1);
140
+ --ctf-border-exempt: rgba(34, 197, 94, 0.3);
141
+ }
142
+
143
+ .ctf-container {
144
+ width: 100%;
145
+ max-width: 1200px;
146
+ margin: 0 auto;
147
+ padding: 2rem 1rem;
148
+ }
149
+
150
+ .ctf-wrapper {
151
+ display: grid;
152
+ grid-template-columns: 1fr 1.5fr;
153
+ gap: 2rem;
154
+ background: var(--ctf-bg-card);
155
+ border: 1px solid var(--ctf-border-card);
156
+ border-radius: 1.5rem;
157
+ padding: 2rem;
158
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
159
+ }
160
+
161
+ .ctf-sidebar {
162
+ display: flex;
163
+ flex-direction: column;
164
+ gap: 1.5rem;
165
+ }
166
+
167
+ .ctf-section-title {
168
+ display: flex;
169
+ align-items: center;
170
+ gap: 0.75rem;
171
+ font-size: 0.875rem;
172
+ font-weight: 700;
173
+ color: var(--ctf-text-secondary);
174
+ text-transform: uppercase;
175
+ margin-bottom: 0.5rem;
176
+ }
177
+
178
+ .ctf-section-title :global(svg) {
179
+ color: var(--ctf-accent);
180
+ }
181
+
182
+ .ctf-input-stack {
183
+ display: flex;
184
+ flex-direction: column;
185
+ gap: 1.25rem;
186
+ }
187
+
188
+ .ctf-input-field {
189
+ display: flex;
190
+ flex-direction: column;
191
+ gap: 0.5rem;
192
+ }
193
+
194
+ .ctf-input-field label {
195
+ font-size: 0.875rem;
196
+ font-weight: 600;
197
+ color: var(--ctf-text-primary);
198
+ }
199
+
200
+ .ctf-input-control {
201
+ padding: 0.75rem;
202
+ padding-right: 2.5rem;
203
+ border: 1px solid var(--ctf-border-input);
204
+ border-radius: 0.5rem;
205
+ background: var(--ctf-bg-input);
206
+ color: var(--ctf-text-primary);
207
+ font-size: 0.875rem;
208
+ transition: all 0.2s;
209
+ }
210
+
211
+ .ctf-input-control:focus {
212
+ outline: none;
213
+ border-color: var(--ctf-accent);
214
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
215
+ }
216
+
217
+ .ctf-main {
218
+ display: flex;
219
+ flex-direction: column;
220
+ gap: 1.5rem;
221
+ }
222
+
223
+ .ctf-result-header {
224
+ display: flex;
225
+ justify-content: space-between;
226
+ align-items: center;
227
+ margin-bottom: 0.5rem;
228
+ }
229
+
230
+ .ctf-badge-exento {
231
+ display: inline-block;
232
+ padding: 0.5rem 1rem;
233
+ background: var(--ctf-bg-exempt);
234
+ border: 1px solid var(--ctf-border-exempt);
235
+ border-radius: 0.5rem;
236
+ font-size: 0.75rem;
237
+ font-weight: 700;
238
+ color: #16a34a;
239
+ }
240
+
241
+ .ctf-total-display {
242
+ display: flex;
243
+ flex-direction: column;
244
+ align-items: center;
245
+ gap: 1rem;
246
+ padding: 2rem;
247
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.05), rgba(99, 102, 241, 0.05));
248
+ border-radius: 1rem;
249
+ text-align: center;
250
+ }
251
+
252
+ .ctf-legal-seal {
253
+ color: var(--ctf-accent);
254
+ opacity: 0.3;
255
+ }
256
+
257
+ .ctf-total-amount {
258
+ font-size: 2.5rem;
259
+ font-weight: 900;
260
+ color: var(--ctf-accent);
261
+ }
262
+
263
+ .ctf-total-label {
264
+ font-size: 0.875rem;
265
+ color: var(--ctf-text-secondary);
266
+ }
267
+
268
+ .ctf-breakdown-list {
269
+ display: flex;
270
+ flex-direction: column;
271
+ gap: 0.75rem;
272
+ padding: 1.5rem;
273
+ background: var(--ctf-bg-input);
274
+ border-radius: 1rem;
275
+ }
276
+
277
+ .ctf-breakdown-item {
278
+ display: flex;
279
+ justify-content: space-between;
280
+ align-items: center;
281
+ padding: 0.75rem 0;
282
+ }
283
+
284
+ .ctf-item-label {
285
+ font-size: 0.875rem;
286
+ color: var(--ctf-text-secondary);
287
+ font-weight: 500;
288
+ }
289
+
290
+ .ctf-item-value {
291
+ font-size: 1rem;
292
+ font-weight: 700;
293
+ color: var(--ctf-text-primary);
294
+ }
295
+
296
+ .ctf-actions-area {
297
+ display: flex;
298
+ gap: 1rem;
299
+ }
300
+
301
+ .ctf-btn-copy {
302
+ display: flex;
303
+ align-items: center;
304
+ justify-content: center;
305
+ gap: 0.75rem;
306
+ padding: 0.875rem 1.5rem;
307
+ background: var(--ctf-accent);
308
+ color: white;
309
+ border: none;
310
+ border-radius: 0.75rem;
311
+ font-size: 0.875rem;
312
+ font-weight: 600;
313
+ cursor: pointer;
314
+ transition: all 0.2s;
315
+ }
316
+
317
+ .ctf-btn-copy:hover {
318
+ background: var(--ctf-accent-hover);
319
+ transform: translateY(-2px);
320
+ box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
321
+ }
322
+
323
+ .ctf-btn-copy:active {
324
+ transform: translateY(0);
325
+ }
326
+
327
+ .ctf-btn-copy :global(svg) {
328
+ width: 1.25rem;
329
+ height: 1.25rem;
330
+ }
331
+
332
+ @media (max-width: 768px) {
333
+ .ctf-wrapper {
334
+ grid-template-columns: 1fr;
335
+ gap: 1.5rem;
336
+ padding: 1.5rem;
337
+ }
338
+ }
339
+ </style>
340
+
341
+ <script>
342
+ import { CourtFeeLogic, type JurisdictionType, type ProcedureType, type EntityType } from './logic';
343
+
344
+ const entity = document.getElementById('entity-type') as HTMLSelectElement | null;
345
+ const juris = document.getElementById('jurisdiction') as HTMLSelectElement | null;
346
+ const procedure = document.getElementById('procedure') as HTMLSelectElement | null;
347
+ const amount = document.getElementById('claim-amount') as HTMLInputElement | null;
348
+ const amountContainer = document.getElementById('amount-container');
349
+
350
+ const totalDisp = document.getElementById('val-total');
351
+ const fixedDisp = document.getElementById('val-fixed');
352
+ const variableDisp = document.getElementById('val-variable');
353
+ const baseDisp = document.getElementById('val-base');
354
+ const exentoTag = document.getElementById('exento-tag');
355
+ const copyBtn = document.getElementById('btn-copy');
356
+
357
+ const container = document.querySelector('.ctf-container') as HTMLElement;
358
+ const currencyLocale = container?.getAttribute('data-currency-locale') || 'es-ES';
359
+ const currencyCode = container?.getAttribute('data-currency-code') || 'EUR';
360
+
361
+ function formatCurrency(val: number): string {
362
+ return new Intl.NumberFormat(currencyLocale, {
363
+ style: 'currency',
364
+ currency: currencyCode,
365
+ }).format(val);
366
+ }
367
+
368
+ function filterProcedureOptions(jurisdictionValue: string): string {
369
+ if (!procedure) return '';
370
+ const opts = procedure.options;
371
+ let first = '';
372
+ for (let i = 0; i < opts.length; i++) {
373
+ const isMatch = opts[i].classList.contains(`opt-${jurisdictionValue}`);
374
+ opts[i].style.display = isMatch ? 'block' : 'none';
375
+ if (isMatch && !first) first = opts[i].value;
376
+ }
377
+ return first;
378
+ }
379
+
380
+ function updateAmountContainer(jurisdictionValue: string) {
381
+ if (amountContainer) {
382
+ const isSocial = jurisdictionValue === 'social';
383
+ amountContainer.style.opacity = isSocial ? '0.3' : '1';
384
+ amountContainer.style.pointerEvents = isSocial ? 'none' : 'auto';
385
+ }
386
+ }
387
+
388
+ function syncProcedureOptions() {
389
+ if (!juris || !procedure) return;
390
+ const first = filterProcedureOptions(juris.value);
391
+ if (first) procedure.value = first;
392
+ updateAmountContainer(juris.value);
393
+ calculate();
394
+ }
395
+
396
+ function displayResults(result: ReturnType<typeof CourtFeeLogic.calculateFee>) {
397
+ if (exentoTag) exentoTag.style.display = result.isExempt ? 'inline-block' : 'none';
398
+ if (totalDisp) totalDisp.textContent = formatCurrency(result.totalFee);
399
+ if (fixedDisp) fixedDisp.textContent = formatCurrency(result.fixedFee);
400
+ if (variableDisp) variableDisp.textContent = formatCurrency(result.variableFee);
401
+ if (baseDisp) baseDisp.textContent = result.isExempt ? 'Exento' : formatCurrency(result.taxableBase);
402
+ }
403
+
404
+ function calculate() {
405
+ if (!entity || !juris || !procedure || !amount) return;
406
+ const result = CourtFeeLogic.calculateFee(
407
+ entity.value as EntityType,
408
+ juris.value as JurisdictionType,
409
+ procedure.value as ProcedureType,
410
+ parseFloat(amount.value) || 0
411
+ );
412
+ displayResults(result);
413
+ }
414
+
415
+ entity?.addEventListener('change', () => calculate());
416
+ juris?.addEventListener('change', syncProcedureOptions);
417
+ procedure?.addEventListener('change', calculate);
418
+ amount?.addEventListener('input', calculate);
419
+
420
+ copyBtn?.addEventListener('click', () => {
421
+ const text = `Total: ${totalDisp?.textContent || '0,00 €'}\nCuota Fija: ${fixedDisp?.textContent || '0,00 €'}\nCuota Variable: ${variableDisp?.textContent || '0,00 €'}`;
422
+ navigator.clipboard.writeText(text);
423
+ });
424
+
425
+ syncProcedureOptions();
426
+ calculate();
427
+ </script>