mantenimento-app 2.2.8 β 2.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/README.md +97 -34
- package/app.js +558 -102
- package/backend/server.js +346 -23
- package/frontend/public/app.js +558 -102
- package/frontend/public/autologin.html +40 -0
- package/frontend/public/index.html +29 -6
- package/frontend/public/styles.css +78 -5
- package/frontend/public/supabase-config.js +4 -11
- package/package.json +5 -1
- package/scripts/auth-url-check.mjs +166 -0
- package/scripts/create-url-login-token.mjs +52 -0
- package/scripts/manage-donor-users.mjs +229 -0
- package/scripts/sql/grant-donor.sql +22 -0
- package/scripts/sql/revoke-donor.sql +19 -0
package/frontend/public/app.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
const defaultExpenseItems = [
|
|
2
2
|
{ label: "π Affitto", help: "Canone mensile di locazione dell'abitazione." },
|
|
3
|
-
{ label: "π‘ Casa (valore locativo)", help: "Valore locativo teorico della casa in uso, se rilevante." },
|
|
4
3
|
{ label: "π‘ Utenze", help: "Luce, gas, acqua, internet e altre utenze domestiche." },
|
|
5
4
|
{ label: "π Cibo/Alimenti", help: "Spesa alimentare mensile imputabile al nucleo familiare." },
|
|
6
5
|
{ label: "π Abbigliamento", help: "Spese medie mensili per abbigliamento dei figli." },
|
|
7
6
|
{ label: "π³ Finanziamenti in corso (extra mutuo)", help: "Rate mensili di finanziamenti diversi dal mutuo." },
|
|
7
|
+
{ label: "π³ Finanziamenti in corso (extra mutuo)", help: "Rate mensili di finanziamenti diversi dal mutuo." },
|
|
8
8
|
{ label: "π Spese macchina", help: "Carburante, assicurazione, bollo, manutenzione ordinaria." },
|
|
9
9
|
{ label: "π’ Amministrazione condominio", help: "Quote condominiali ordinarie e costi amministrativi ricorrenti." },
|
|
10
10
|
{ label: "π Spese scolastiche/mensa", help: "Costi scolastici ricorrenti, mensa e contributi periodici." },
|
|
@@ -59,8 +59,8 @@ const defaultExpenseItems = [
|
|
|
59
59
|
heroSubtitle: "Modello di calcolo orientativo adattato a <strong>reddito netto mensile/annuale</strong> oppure a <strong>CU (lordo annuale con stima del netto)</strong>, con inserimento spese separato per <strong>Coniuge 1</strong> e <strong>Coniuge 2</strong>.",
|
|
60
60
|
quickActions: "Azioni rapide",
|
|
61
61
|
btnReset: "Reset valori",
|
|
62
|
-
btnExport: "Esporta JSON
|
|
63
|
-
btnImport: "Carica JSON
|
|
62
|
+
btnExport: "Esporta JSON",
|
|
63
|
+
btnImport: "Carica JSON",
|
|
64
64
|
btnPdf: "Genera e scarica PDF",
|
|
65
65
|
btnZoomReset: "Reset",
|
|
66
66
|
btnZoomOutTitle: "Riduci zoom",
|
|
@@ -133,6 +133,12 @@ const defaultExpenseItems = [
|
|
|
133
133
|
authEmailNotVerified: "Email non verificata. Apri la mail di conferma e completa la verifica, poi riprova il login.",
|
|
134
134
|
authInvalidCredentials: "Login fallito: credenziali non valide o utente non presente nel nuovo cloud. Usa Registrati solo se l'utente non e mai stato creato su Supabase.",
|
|
135
135
|
authLoginFailed: "Login fallito: {message}",
|
|
136
|
+
authUrlLoginHttpsOnly: "Login da URL consentito solo in HTTPS.",
|
|
137
|
+
authUrlLoginMissingParams: "Login da URL ignorato: servono utente (o email) e password.",
|
|
138
|
+
authUrlLoginStarted: "Login automatico sicuro in corso...",
|
|
139
|
+
authUrlLoginPasswordDisabled: "Login da URL con password disabilitato per sicurezza. Usa authToken monouso.",
|
|
140
|
+
authUrlLoginTokenMissing: "Login da URL ignorato: manca authToken.",
|
|
141
|
+
authUrlLoginTokenExchangeFailed: "Login automatico sicuro fallito: {message}",
|
|
136
142
|
authUserFallback: "utente",
|
|
137
143
|
authLogoutDone: "Logout eseguito.",
|
|
138
144
|
authLoginRequired: "Effettua prima il login.",
|
|
@@ -220,12 +226,21 @@ const defaultExpenseItems = [
|
|
|
220
226
|
pdfPopupBlocked: "Il popup Γ¨ stato bloccato dal browser. Consenti i popup per questo sito e riprova.",
|
|
221
227
|
authLoginBeforeExportStatus: "Effettua il login KeyLock prima di esportare JSON cifrato.",
|
|
222
228
|
authLoginBeforeExportAlert: "Per esportare, devi prima fare login KeyLock.",
|
|
223
|
-
authInvalidJsonFormat: "Formato JSON non valido: e richiesto un export cifrato
|
|
229
|
+
authInvalidJsonFormat: "Formato JSON non valido: e richiesto un export KeyLock valido (cifrato o non cifrato)",
|
|
224
230
|
authLoginBeforeImport: "Effettua prima il login KeyLock per importare il file cifrato",
|
|
225
231
|
authFileOwnedByOther: "Questo file appartiene a un altro utente KeyLock",
|
|
226
232
|
authDecryptedContentInvalid: "Contenuto decifrato non valido",
|
|
227
233
|
authEncryptedJsonImported: "JSON cifrato importato correttamente.",
|
|
228
234
|
authEncryptedJsonLoaded: "Dati caricati da JSON cifrato.",
|
|
235
|
+
authExportModalTitle: "Esportazione JSON",
|
|
236
|
+
authExportModalWarning: "Per impostazione predefinita il file viene cifrato e puo essere importato solo dallo stesso utente KeyLock.",
|
|
237
|
+
authExportPlainCheckbox: "Esporta JSON non cifrato (compatibile con altri utenti)",
|
|
238
|
+
authExportPlainRisk: "Conferma esplicita richiesta: il file non cifrato puo essere letto da chiunque.",
|
|
239
|
+
authExportModalCancel: "Annulla",
|
|
240
|
+
authExportModalConfirm: "Conferma export",
|
|
241
|
+
authExportPlainFileWarning: "Attenzione: stai esportando un JSON non cifrato.",
|
|
242
|
+
authPlainJsonImported: "JSON non cifrato importato correttamente.",
|
|
243
|
+
authPlainJsonLoaded: "Dati caricati da JSON non cifrato.",
|
|
229
244
|
authImportFailedStatus: "Import JSON fallito: {message}",
|
|
230
245
|
authImportFailedAlert: "Impossibile caricare il JSON: {message}",
|
|
231
246
|
spouse1Default: "Coniuge 1",
|
|
@@ -264,11 +279,13 @@ const defaultExpenseItems = [
|
|
|
264
279
|
extraAnnHint2: "Quota annuale straordinaria stimata a carico di {spouse} (es. sanitarie non ricorrenti, scolastiche extra, attivitΓ non ordinarie).",
|
|
265
280
|
extraMonthlyEstimate: "Quota mensile stimata: {amount}",
|
|
266
281
|
firstHomeBoxTitle: "π‘ Mutuo prima casa ceduta",
|
|
267
|
-
firstHomeBoxNote: "Dichiara
|
|
268
|
-
firstHomeMortgageEnabledLabel: "
|
|
269
|
-
firstHomeMortgageEnabledHint: "Attiva per includere il mutuo
|
|
282
|
+
firstHomeBoxNote: "Dichiara i benefici economici della prima casa assegnata: il modello considera sia il mutuo ceduto sia il valore locativo quando la casa e assegnata al collocatario.",
|
|
283
|
+
firstHomeMortgageEnabledLabel: "Benefici prima casa dei coniugi",
|
|
284
|
+
firstHomeMortgageEnabledHint: "Attiva per includere tra i benefici compensativi il mutuo ceduto e/o il valore locativo della casa assegnata.",
|
|
270
285
|
firstHomeMortgageAmountLabel: "Rata mutuo mensile ({currency})",
|
|
271
286
|
firstHomeMortgageAmountHint: "Importo mensile complessivo della rata del mutuo prima casa.",
|
|
287
|
+
firstHomeRentalValueLabel: "Casa (valore locativo) ({currency})",
|
|
288
|
+
firstHomeRentalValueHint: "Valore locativo mensile della casa assegnata, usato per valorizzare il beneficio economico implicito.",
|
|
272
289
|
firstHomeAssignedToLabel: "Casa assegnata a",
|
|
273
290
|
firstHomeAssignedToHint: "Seleziona il coniuge a cui e ceduta la prima casa.",
|
|
274
291
|
firstHomeAssignedToNone: "Nessuna cessione",
|
|
@@ -280,14 +297,17 @@ const defaultExpenseItems = [
|
|
|
280
297
|
calcNoTransferWithBenefits: "Nessun trasferimento monetario suggerito. Benefici gia allocati: {benefits}.",
|
|
281
298
|
calcBenefitFamilyAllowance: "Assegno familiare INPS percepito da {spouse}",
|
|
282
299
|
calcBenefitPrimaryHomeMortgage: "Quota mutuo prima casa ceduta al collocatario ({payer} -> {receiver})",
|
|
300
|
+
calcBenefitPrimaryHomeAssignment: "Assegnazione casa familiare (valore locativo) ({payer} -> {receiver})",
|
|
283
301
|
pdfCompBenefitsSection: "Benefici compensativi gia allocati",
|
|
284
302
|
pdfCompBenefitsItem: "Beneficio",
|
|
285
303
|
pdfCompBenefitsAmount: "Valore {currency}/mese",
|
|
286
304
|
pdfCompBenefitsNone: "Nessun beneficio compensativo aggiuntivo dichiarato.",
|
|
305
|
+
pdfCompBenefitsTotal: "Totale benefici allocati",
|
|
287
306
|
pdfPrimaryHomeMortgage: "Mutuo prima casa ceduta",
|
|
288
307
|
pdfPrimaryHomeNotDeclared: "Non dichiarato",
|
|
289
308
|
pdfPrimaryHomeAssignedTo: "Assegnata a",
|
|
290
309
|
pdfPrimaryHomeMonthlyAmount: "Rata mensile",
|
|
310
|
+
pdfPrimaryHomeRentalValue: "Casa (valore locativo)",
|
|
291
311
|
pdfPrimaryHomeSplit: "Ripartizione mutuo",
|
|
292
312
|
pdfPrimaryHomeAppliedOnlyColl: "Considerato solo se casa ceduta al collocatario.",
|
|
293
313
|
pdfExtraordinaryRow: "Spese straordinarie (quota mensile da annuo)",
|
|
@@ -401,8 +421,8 @@ const defaultExpenseItems = [
|
|
|
401
421
|
heroSubtitle: "Indicative calculation model based on <strong>monthly/yearly net income</strong> or <strong>CU gross yearly income (estimated monthly net)</strong>, with separate expense input for <strong>Spouse 1</strong> and <strong>Spouse 2</strong>.",
|
|
402
422
|
quickActions: "Quick actions",
|
|
403
423
|
btnReset: "Reset values",
|
|
404
|
-
btnExport: "Export
|
|
405
|
-
btnImport: "Import
|
|
424
|
+
btnExport: "Export JSON",
|
|
425
|
+
btnImport: "Import JSON",
|
|
406
426
|
btnPdf: "Generate and download PDF",
|
|
407
427
|
btnZoomReset: "Reset",
|
|
408
428
|
btnZoomOutTitle: "Zoom out",
|
|
@@ -475,6 +495,12 @@ const defaultExpenseItems = [
|
|
|
475
495
|
authEmailNotVerified: "Email not verified. Open your confirmation email and complete verification, then retry login.",
|
|
476
496
|
authInvalidCredentials: "Login failed: invalid credentials or user not found in this cloud. Use Register only if the user was never created in Supabase.",
|
|
477
497
|
authLoginFailed: "Login failed: {message}",
|
|
498
|
+
authUrlLoginHttpsOnly: "URL login is allowed only over HTTPS.",
|
|
499
|
+
authUrlLoginMissingParams: "URL login ignored: username/email and password are required.",
|
|
500
|
+
authUrlLoginStarted: "Secure automatic URL login in progress...",
|
|
501
|
+
authUrlLoginPasswordDisabled: "Password-in-URL login is disabled for security. Use one-time authToken.",
|
|
502
|
+
authUrlLoginTokenMissing: "URL login ignored: authToken is missing.",
|
|
503
|
+
authUrlLoginTokenExchangeFailed: "Secure automatic login failed: {message}",
|
|
478
504
|
authUserFallback: "user",
|
|
479
505
|
authLogoutDone: "Logout completed.",
|
|
480
506
|
authLoginRequired: "Please login first.",
|
|
@@ -562,12 +588,21 @@ const defaultExpenseItems = [
|
|
|
562
588
|
pdfPopupBlocked: "The popup was blocked by the browser. Allow popups for this site and try again.",
|
|
563
589
|
authLoginBeforeExportStatus: "Please login to KeyLock before exporting encrypted JSON.",
|
|
564
590
|
authLoginBeforeExportAlert: "You must login to KeyLock before exporting.",
|
|
565
|
-
authInvalidJsonFormat: "Invalid JSON format:
|
|
591
|
+
authInvalidJsonFormat: "Invalid JSON format: a valid KeyLock export is required (encrypted or unencrypted)",
|
|
566
592
|
authLoginBeforeImport: "Login to KeyLock before importing the encrypted file",
|
|
567
593
|
authFileOwnedByOther: "This file belongs to another KeyLock user",
|
|
568
594
|
authDecryptedContentInvalid: "Invalid decrypted content",
|
|
569
595
|
authEncryptedJsonImported: "Encrypted JSON imported successfully.",
|
|
570
596
|
authEncryptedJsonLoaded: "Data loaded from encrypted JSON.",
|
|
597
|
+
authExportModalTitle: "JSON export",
|
|
598
|
+
authExportModalWarning: "By default, the file is encrypted and can only be imported by the same KeyLock user.",
|
|
599
|
+
authExportPlainCheckbox: "Export unencrypted JSON (compatible with other users)",
|
|
600
|
+
authExportPlainRisk: "Explicit confirmation required: unencrypted files can be read by anyone.",
|
|
601
|
+
authExportModalCancel: "Cancel",
|
|
602
|
+
authExportModalConfirm: "Confirm export",
|
|
603
|
+
authExportPlainFileWarning: "Warning: you are exporting an unencrypted JSON file.",
|
|
604
|
+
authPlainJsonImported: "Unencrypted JSON imported successfully.",
|
|
605
|
+
authPlainJsonLoaded: "Data loaded from unencrypted JSON.",
|
|
571
606
|
authImportFailedStatus: "JSON import failed: {message}",
|
|
572
607
|
authImportFailedAlert: "Unable to load JSON: {message}",
|
|
573
608
|
spouse1Default: "Spouse 1",
|
|
@@ -606,11 +641,13 @@ const defaultExpenseItems = [
|
|
|
606
641
|
extraAnnHint2: "Estimated yearly extraordinary share for {spouse} (e.g., non-recurring medical, extra school, non-ordinary activities).",
|
|
607
642
|
extraMonthlyEstimate: "Estimated monthly share: {amount}",
|
|
608
643
|
firstHomeBoxTitle: "π‘ Assigned primary home mortgage",
|
|
609
|
-
firstHomeBoxNote: "Declare
|
|
610
|
-
firstHomeMortgageEnabledLabel: "
|
|
611
|
-
firstHomeMortgageEnabledHint: "Enable to include the
|
|
644
|
+
firstHomeBoxNote: "Declare the economic benefits linked to the assigned primary home: the model counts both the transferred mortgage and the rental value when the home is assigned to the custodial parent.",
|
|
645
|
+
firstHomeMortgageEnabledLabel: "Primary-home benefits",
|
|
646
|
+
firstHomeMortgageEnabledHint: "Enable to include in compensative benefits the transferred mortgage and/or the rental value of the assigned home.",
|
|
612
647
|
firstHomeMortgageAmountLabel: "Monthly mortgage payment ({currency})",
|
|
613
648
|
firstHomeMortgageAmountHint: "Total monthly amount of the primary-home mortgage payment.",
|
|
649
|
+
firstHomeRentalValueLabel: "Home (rental value) ({currency})",
|
|
650
|
+
firstHomeRentalValueHint: "Monthly rental value of the assigned home, used to quantify the implicit economic benefit.",
|
|
614
651
|
firstHomeAssignedToLabel: "Home assigned to",
|
|
615
652
|
firstHomeAssignedToHint: "Select which spouse receives assignment of the primary home.",
|
|
616
653
|
firstHomeAssignedToNone: "No assignment",
|
|
@@ -622,14 +659,17 @@ const defaultExpenseItems = [
|
|
|
622
659
|
calcNoTransferWithBenefits: "No monetary transfer suggested. Already allocated benefits: {benefits}.",
|
|
623
660
|
calcBenefitFamilyAllowance: "INPS family allowance received by {spouse}",
|
|
624
661
|
calcBenefitPrimaryHomeMortgage: "Primary-home mortgage share assigned to custodial parent ({payer} -> {receiver})",
|
|
662
|
+
calcBenefitPrimaryHomeAssignment: "Assigned family home (rental value) ({payer} -> {receiver})",
|
|
625
663
|
pdfCompBenefitsSection: "Compensative benefits already allocated",
|
|
626
664
|
pdfCompBenefitsItem: "Benefit",
|
|
627
665
|
pdfCompBenefitsAmount: "Value {currency}/month",
|
|
628
666
|
pdfCompBenefitsNone: "No additional compensative benefits declared.",
|
|
667
|
+
pdfCompBenefitsTotal: "Total allocated benefits",
|
|
629
668
|
pdfPrimaryHomeMortgage: "Assigned primary-home mortgage",
|
|
630
669
|
pdfPrimaryHomeNotDeclared: "Not declared",
|
|
631
670
|
pdfPrimaryHomeAssignedTo: "Assigned to",
|
|
632
671
|
pdfPrimaryHomeMonthlyAmount: "Monthly payment",
|
|
672
|
+
pdfPrimaryHomeRentalValue: "Home (rental value)",
|
|
633
673
|
pdfPrimaryHomeSplit: "Mortgage split",
|
|
634
674
|
pdfPrimaryHomeAppliedOnlyColl: "Counted only when the home is assigned to the custodial parent.",
|
|
635
675
|
pdfExtraordinaryRow: "Extraordinary expenses (monthly share from yearly)",
|
|
@@ -745,12 +785,13 @@ const defaultExpenseItems = [
|
|
|
745
785
|
&& !SUPABASE_ANON_KEY.includes("YOUR_PUBLIC_ANON_KEY")
|
|
746
786
|
? window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
|
747
787
|
auth: {
|
|
748
|
-
persistSession:
|
|
749
|
-
autoRefreshToken:
|
|
788
|
+
persistSession: true,
|
|
789
|
+
autoRefreshToken: true,
|
|
750
790
|
detectSessionInUrl: false
|
|
751
791
|
}
|
|
752
792
|
})
|
|
753
793
|
: null;
|
|
794
|
+
const PROFILE_KEY_STORAGE_PREFIX = "m_profile_key_";
|
|
754
795
|
const authSession = {
|
|
755
796
|
username: null,
|
|
756
797
|
email: null,
|
|
@@ -793,6 +834,7 @@ const defaultExpenseItems = [
|
|
|
793
834
|
month: "",
|
|
794
835
|
byMonth: {}
|
|
795
836
|
};
|
|
837
|
+
let speseConvivenzaAutoMode = true;
|
|
796
838
|
let currentLang = "it";
|
|
797
839
|
let currentCurrency = "EUR";
|
|
798
840
|
const CALC_API_BASE_STORAGE_KEY = "keylock_calc_api_base";
|
|
@@ -1152,6 +1194,12 @@ const defaultExpenseItems = [
|
|
|
1152
1194
|
const btnZoomReset = document.getElementById("btnZoomReset");
|
|
1153
1195
|
const btnZoomOut = document.getElementById("btnZoomOut");
|
|
1154
1196
|
const btnZoomIn = document.getElementById("btnZoomIn");
|
|
1197
|
+
const exportModeModalTitle = document.getElementById("exportModeModalTitle");
|
|
1198
|
+
const exportModeModalWarning = document.getElementById("exportModeModalWarning");
|
|
1199
|
+
const lblExportPlainJson = document.getElementById("lblExportPlainJson");
|
|
1200
|
+
const exportModeModalRisk = document.getElementById("exportModeModalRisk");
|
|
1201
|
+
const btnCancelExportMode = document.getElementById("btnCancelExportMode");
|
|
1202
|
+
const btnConfirmExportMode = document.getElementById("btnConfirmExportMode");
|
|
1155
1203
|
const lblLang = document.getElementById("lblLang");
|
|
1156
1204
|
const lblCurrency = document.getElementById("lblCurrency");
|
|
1157
1205
|
const authHead = document.getElementById("authHead");
|
|
@@ -1174,6 +1222,8 @@ const defaultExpenseItems = [
|
|
|
1174
1222
|
const hintPrimaCasaMutuoEnabled = document.getElementById("hintPrimaCasaMutuoEnabled");
|
|
1175
1223
|
const lblPrimaCasaMutuoImporto = document.getElementById("lblPrimaCasaMutuoImporto");
|
|
1176
1224
|
const hintPrimaCasaMutuoImporto = document.getElementById("hintPrimaCasaMutuoImporto");
|
|
1225
|
+
const lblPrimaCasaValoreLocativo = document.getElementById("lblPrimaCasaValoreLocativo");
|
|
1226
|
+
const hintPrimaCasaValoreLocativo = document.getElementById("hintPrimaCasaValoreLocativo");
|
|
1177
1227
|
const lblPrimaCasaAssegnataA = document.getElementById("lblPrimaCasaAssegnataA");
|
|
1178
1228
|
const hintPrimaCasaAssegnataA = document.getElementById("hintPrimaCasaAssegnataA");
|
|
1179
1229
|
const lblPrimaCasaMutuoPerc1 = document.getElementById("lblPrimaCasaMutuoPerc1");
|
|
@@ -1197,6 +1247,12 @@ const defaultExpenseItems = [
|
|
|
1197
1247
|
if (btnExport) btnExport.textContent = tr("btnExport");
|
|
1198
1248
|
if (btnImport) btnImport.textContent = tr("btnImport");
|
|
1199
1249
|
if (btnPdf) btnPdf.textContent = tr("btnPdf");
|
|
1250
|
+
if (exportModeModalTitle) exportModeModalTitle.textContent = tr("authExportModalTitle");
|
|
1251
|
+
if (exportModeModalWarning) exportModeModalWarning.textContent = tr("authExportModalWarning");
|
|
1252
|
+
if (lblExportPlainJson) lblExportPlainJson.textContent = tr("authExportPlainCheckbox");
|
|
1253
|
+
if (exportModeModalRisk) exportModeModalRisk.textContent = tr("authExportPlainRisk");
|
|
1254
|
+
if (btnCancelExportMode) btnCancelExportMode.textContent = tr("authExportModalCancel");
|
|
1255
|
+
if (btnConfirmExportMode) btnConfirmExportMode.textContent = tr("authExportModalConfirm");
|
|
1200
1256
|
if (btnZoomReset) btnZoomReset.textContent = tr("btnZoomReset");
|
|
1201
1257
|
if (btnZoomOut) btnZoomOut.title = tr("btnZoomOutTitle");
|
|
1202
1258
|
if (btnZoomIn) btnZoomIn.title = tr("btnZoomInTitle");
|
|
@@ -1231,6 +1287,8 @@ const defaultExpenseItems = [
|
|
|
1231
1287
|
if (hintPrimaCasaMutuoEnabled) hintPrimaCasaMutuoEnabled.title = tr("firstHomeMortgageEnabledHint");
|
|
1232
1288
|
if (lblPrimaCasaMutuoImporto) lblPrimaCasaMutuoImporto.textContent = msg("firstHomeMortgageAmountLabel", { currency: currentCurrency });
|
|
1233
1289
|
if (hintPrimaCasaMutuoImporto) hintPrimaCasaMutuoImporto.title = tr("firstHomeMortgageAmountHint");
|
|
1290
|
+
if (lblPrimaCasaValoreLocativo) lblPrimaCasaValoreLocativo.textContent = msg("firstHomeRentalValueLabel", { currency: currentCurrency });
|
|
1291
|
+
if (hintPrimaCasaValoreLocativo) hintPrimaCasaValoreLocativo.title = tr("firstHomeRentalValueHint");
|
|
1234
1292
|
if (lblPrimaCasaAssegnataA) lblPrimaCasaAssegnataA.textContent = tr("firstHomeAssignedToLabel");
|
|
1235
1293
|
if (hintPrimaCasaAssegnataA) hintPrimaCasaAssegnataA.title = tr("firstHomeAssignedToHint");
|
|
1236
1294
|
if (lblPrimaCasaMutuoPerc1) lblPrimaCasaMutuoPerc1.textContent = msg("firstHomeSplitLabel", { spouse: c1n() });
|
|
@@ -1598,6 +1656,32 @@ const defaultExpenseItems = [
|
|
|
1598
1656
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value || "").trim());
|
|
1599
1657
|
}
|
|
1600
1658
|
|
|
1659
|
+
function userHasServerDonorEntitlement(user) {
|
|
1660
|
+
const safeUser = user && typeof user === "object" ? user : {};
|
|
1661
|
+
const userMeta = safeUser.user_metadata && typeof safeUser.user_metadata === "object"
|
|
1662
|
+
? safeUser.user_metadata
|
|
1663
|
+
: {};
|
|
1664
|
+
const appMeta = safeUser.app_metadata && typeof safeUser.app_metadata === "object"
|
|
1665
|
+
? safeUser.app_metadata
|
|
1666
|
+
: {};
|
|
1667
|
+
|
|
1668
|
+
const truthy = (value) => {
|
|
1669
|
+
if (value === true || value === 1) return true;
|
|
1670
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
1671
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
1672
|
+
};
|
|
1673
|
+
|
|
1674
|
+
const roleValue = String(userMeta.role || appMeta.role || "").trim().toLowerCase();
|
|
1675
|
+
if (roleValue === "donor" || roleValue === "premium") return true;
|
|
1676
|
+
|
|
1677
|
+
return truthy(userMeta.is_donor)
|
|
1678
|
+
|| truthy(userMeta.isDonor)
|
|
1679
|
+
|| truthy(userMeta.premium)
|
|
1680
|
+
|| truthy(appMeta.is_donor)
|
|
1681
|
+
|| truthy(appMeta.isDonor)
|
|
1682
|
+
|| truthy(appMeta.premium);
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1601
1685
|
const PRIMARY_PSEUDO_EMAIL_DOMAIN = "keylock-auth.app";
|
|
1602
1686
|
function buildPseudoEmailCandidates(username) {
|
|
1603
1687
|
const base = normalizeUsername(username);
|
|
@@ -1697,7 +1781,8 @@ const defaultExpenseItems = [
|
|
|
1697
1781
|
authSession.email = normalizeEmail(user && user.email ? user.email : "");
|
|
1698
1782
|
authSession.userId = user.id;
|
|
1699
1783
|
authSession.keyBits = await deriveSessionKeyBits(password, user.id);
|
|
1700
|
-
authSession.isDonor =
|
|
1784
|
+
authSession.isDonor = userHasServerDonorEntitlement(user);
|
|
1785
|
+
saveProfileKeyForUser(user.id, authSession.keyBits);
|
|
1701
1786
|
updateAuthUi();
|
|
1702
1787
|
return msg("authLoginAs", { username });
|
|
1703
1788
|
}
|
|
@@ -1719,6 +1804,32 @@ const defaultExpenseItems = [
|
|
|
1719
1804
|
return out;
|
|
1720
1805
|
}
|
|
1721
1806
|
|
|
1807
|
+
function saveProfileKeyForUser(userId, keyBits) {
|
|
1808
|
+
try {
|
|
1809
|
+
if (!userId || !(keyBits instanceof Uint8Array) || keyBits.length !== 32) return;
|
|
1810
|
+
localStorage.setItem(`${PROFILE_KEY_STORAGE_PREFIX}${userId}`, bytesToBase64(keyBits));
|
|
1811
|
+
} catch (_) {}
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
function loadProfileKeyForUser(userId) {
|
|
1815
|
+
try {
|
|
1816
|
+
if (!userId) return null;
|
|
1817
|
+
const stored = String(localStorage.getItem(`${PROFILE_KEY_STORAGE_PREFIX}${userId}`) || "").trim();
|
|
1818
|
+
if (!stored) return null;
|
|
1819
|
+
const bytes = base64ToBytes(stored);
|
|
1820
|
+
return bytes && bytes.length === 32 ? bytes : null;
|
|
1821
|
+
} catch (_) {
|
|
1822
|
+
return null;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
function clearProfileKeyForUser(userId) {
|
|
1827
|
+
try {
|
|
1828
|
+
if (!userId) return;
|
|
1829
|
+
localStorage.removeItem(`${PROFILE_KEY_STORAGE_PREFIX}${userId}`);
|
|
1830
|
+
} catch (_) {}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1722
1833
|
function decodeBytesFlexible(value) {
|
|
1723
1834
|
const raw = String(value || "").trim();
|
|
1724
1835
|
if (!raw) return null;
|
|
@@ -2480,7 +2591,164 @@ const defaultExpenseItems = [
|
|
|
2480
2591
|
}
|
|
2481
2592
|
}
|
|
2482
2593
|
|
|
2594
|
+
function clearSensitiveAuthQueryParams() {
|
|
2595
|
+
try {
|
|
2596
|
+
const currentUrl = new URL(window.location.href);
|
|
2597
|
+
const params = currentUrl.searchParams;
|
|
2598
|
+
const hashParams = new URLSearchParams(String(currentUrl.hash || "").replace(/^#/, ""));
|
|
2599
|
+
const keys = ["autologin", "authUser", "authEmail", "authPass", "authPass64", "authToken"];
|
|
2600
|
+
let changed = false;
|
|
2601
|
+
keys.forEach((key) => {
|
|
2602
|
+
if (params.has(key)) {
|
|
2603
|
+
params.delete(key);
|
|
2604
|
+
changed = true;
|
|
2605
|
+
}
|
|
2606
|
+
if (hashParams.has(key)) {
|
|
2607
|
+
hashParams.delete(key);
|
|
2608
|
+
changed = true;
|
|
2609
|
+
}
|
|
2610
|
+
});
|
|
2611
|
+
if (!changed) return;
|
|
2612
|
+
const nextSearch = params.toString();
|
|
2613
|
+
const nextHash = hashParams.toString();
|
|
2614
|
+
const nextUrl = `${currentUrl.pathname}${nextSearch ? `?${nextSearch}` : ""}${nextHash ? `#${nextHash}` : ""}`;
|
|
2615
|
+
window.history.replaceState({}, "", nextUrl);
|
|
2616
|
+
} catch (_) {}
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
async function restorePersistedAuthSession() {
|
|
2620
|
+
try {
|
|
2621
|
+
if (!(supabaseClient && supabaseClient.auth && typeof supabaseClient.auth.getSession === "function")) return;
|
|
2622
|
+
const sessionRes = await supabaseClient.auth.getSession();
|
|
2623
|
+
const session = sessionRes && sessionRes.data ? sessionRes.data.session : null;
|
|
2624
|
+
const user = session && session.user ? session.user : null;
|
|
2625
|
+
if (!user || !user.id) return;
|
|
2626
|
+
|
|
2627
|
+
const userMeta = user && user.user_metadata ? user.user_metadata : {};
|
|
2628
|
+
const effectiveUsername = normalizeUsername(userMeta.username || "")
|
|
2629
|
+
|| normalizeUsername(String(user.email || "").split("@")[0])
|
|
2630
|
+
|| tr("authUserFallback");
|
|
2631
|
+
|
|
2632
|
+
authSession.username = effectiveUsername;
|
|
2633
|
+
authSession.email = normalizeEmail(user.email || "");
|
|
2634
|
+
authSession.userId = user.id;
|
|
2635
|
+
authSession.keyBits = loadProfileKeyForUser(user.id);
|
|
2636
|
+
authSession.isDonor = userHasServerDonorEntitlement(user);
|
|
2637
|
+
updateAuthUi();
|
|
2638
|
+
|
|
2639
|
+
if (authSession.keyBits) {
|
|
2640
|
+
await loadScenarioForLoggedUser({ silentNoData: true, fromLogin: true });
|
|
2641
|
+
}
|
|
2642
|
+
} catch (_) {}
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
async function maybeAutoLoginFromUrl() {
|
|
2646
|
+
try {
|
|
2647
|
+
const params = new URLSearchParams(window.location.search || "");
|
|
2648
|
+
const hasAnyAutoAuthParam = ["autologin", "authUser", "authEmail", "authPass", "authPass64", "authToken"].some((key) => params.has(key));
|
|
2649
|
+
if (!hasAnyAutoAuthParam) return;
|
|
2650
|
+
|
|
2651
|
+
const force = String(params.get("autologin") || "1").trim().toLowerCase();
|
|
2652
|
+
if (!(force === "1" || force === "true" || force === "yes" || force === "on")) return;
|
|
2653
|
+
|
|
2654
|
+
const isHttps = window.location.protocol === "https:" || window.location.hostname === "localhost";
|
|
2655
|
+
if (!isHttps) {
|
|
2656
|
+
clearSensitiveAuthQueryParams();
|
|
2657
|
+
setAuthStatus(tr("authUrlLoginHttpsOnly"), true);
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
const hasLegacyPasswordParams = params.has("authPass") || params.has("authPass64") || params.has("authUser") || params.has("authEmail");
|
|
2662
|
+
if (hasLegacyPasswordParams) {
|
|
2663
|
+
clearSensitiveAuthQueryParams();
|
|
2664
|
+
setAuthStatus(tr("authUrlLoginPasswordDisabled"), true);
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
const authToken = String(params.get("authToken") || "").trim();
|
|
2669
|
+
const hashParams = new URLSearchParams(String(window.location.hash || "").replace(/^#/, ""));
|
|
2670
|
+
const hashToken = String(hashParams.get("authToken") || "").trim();
|
|
2671
|
+
const tokenForExchange = authToken || hashToken;
|
|
2672
|
+
if (!tokenForExchange) {
|
|
2673
|
+
clearSensitiveAuthQueryParams();
|
|
2674
|
+
setAuthStatus(tr("authUrlLoginTokenMissing"), true);
|
|
2675
|
+
return;
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
const apiBase = resolveCalculationApiBase();
|
|
2679
|
+
if (!apiBase) {
|
|
2680
|
+
clearSensitiveAuthQueryParams();
|
|
2681
|
+
setAuthStatus(msg("authUrlLoginTokenExchangeFailed", { message: "API base non configurata" }), true);
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
setAuthMode("login");
|
|
2686
|
+
setAuthStatus(tr("authUrlLoginStarted"), false);
|
|
2687
|
+
const response = await fetch(`${apiBase}/api/auth/url-login/exchange`, {
|
|
2688
|
+
method: "POST",
|
|
2689
|
+
headers: {
|
|
2690
|
+
"Content-Type": "application/json"
|
|
2691
|
+
},
|
|
2692
|
+
body: JSON.stringify({ token: tokenForExchange })
|
|
2693
|
+
});
|
|
2694
|
+
|
|
2695
|
+
const payload = await response.json().catch(() => ({}));
|
|
2696
|
+
if (!response.ok || !payload || !payload.ok || !payload.session) {
|
|
2697
|
+
clearSensitiveAuthQueryParams();
|
|
2698
|
+
const reason = String(payload && payload.error ? payload.error : `HTTP ${response.status}`);
|
|
2699
|
+
setAuthStatus(msg("authUrlLoginTokenExchangeFailed", { message: reason }), true);
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
if (!(supabaseClient && supabaseClient.auth && typeof supabaseClient.auth.setSession === "function")) {
|
|
2704
|
+
clearSensitiveAuthQueryParams();
|
|
2705
|
+
setAuthStatus(msg("authUrlLoginTokenExchangeFailed", { message: "Supabase non configurato" }), true);
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
const accessToken = String(payload.session.access_token || "");
|
|
2710
|
+
const refreshToken = String(payload.session.refresh_token || "");
|
|
2711
|
+
if (!accessToken || !refreshToken) {
|
|
2712
|
+
clearSensitiveAuthQueryParams();
|
|
2713
|
+
setAuthStatus(msg("authUrlLoginTokenExchangeFailed", { message: "sessione non valida" }), true);
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
const setSessionRes = await supabaseClient.auth.setSession({
|
|
2718
|
+
access_token: accessToken,
|
|
2719
|
+
refresh_token: refreshToken
|
|
2720
|
+
});
|
|
2721
|
+
if (setSessionRes.error || !setSessionRes.data || !setSessionRes.data.user || !setSessionRes.data.session) {
|
|
2722
|
+
clearSensitiveAuthQueryParams();
|
|
2723
|
+
const reason = String(setSessionRes.error && setSessionRes.error.message ? setSessionRes.error.message : "sessione Supabase non impostabile");
|
|
2724
|
+
setAuthStatus(msg("authUrlLoginTokenExchangeFailed", { message: reason }), true);
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
const user = setSessionRes.data.user;
|
|
2729
|
+
const userMeta = user && user.user_metadata ? user.user_metadata : {};
|
|
2730
|
+
const effectiveUsername = normalizeUsername(userMeta.username || "")
|
|
2731
|
+
|| normalizeUsername(String(user.email || "").split("@")[0])
|
|
2732
|
+
|| tr("authUserFallback");
|
|
2733
|
+
const profileKeyBytes = decodeBytesFlexible(payload.session.profile_key || "");
|
|
2734
|
+
|
|
2735
|
+
authSession.username = effectiveUsername;
|
|
2736
|
+
authSession.email = normalizeEmail(user.email || "");
|
|
2737
|
+
authSession.userId = user.id;
|
|
2738
|
+
authSession.keyBits = profileKeyBytes && profileKeyBytes.length === 32
|
|
2739
|
+
? profileKeyBytes
|
|
2740
|
+
: await deriveSessionKeyBits(tokenForExchange, user.id);
|
|
2741
|
+
authSession.isDonor = userHasServerDonorEntitlement(user);
|
|
2742
|
+
saveProfileKeyForUser(user.id, authSession.keyBits);
|
|
2743
|
+
updateAuthUi();
|
|
2744
|
+
await loadScenarioForLoggedUser({ silentNoData: true, fromLogin: true });
|
|
2745
|
+
|
|
2746
|
+
clearSensitiveAuthQueryParams();
|
|
2747
|
+
} catch (_) {}
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2483
2750
|
async function logoutKeyLockUser() {
|
|
2751
|
+
const previousUserId = authSession.userId;
|
|
2484
2752
|
if (supabaseClient) {
|
|
2485
2753
|
await supabaseClient.auth.signOut();
|
|
2486
2754
|
}
|
|
@@ -2489,6 +2757,7 @@ const defaultExpenseItems = [
|
|
|
2489
2757
|
authSession.userId = null;
|
|
2490
2758
|
authSession.keyBits = null;
|
|
2491
2759
|
authSession.isDonor = false;
|
|
2760
|
+
clearProfileKeyForUser(previousUserId);
|
|
2492
2761
|
cloudProfileSession.loaded = null;
|
|
2493
2762
|
cloudProfileSession.history = [];
|
|
2494
2763
|
updateAuthUi();
|
|
@@ -3093,6 +3362,27 @@ const defaultExpenseItems = [
|
|
|
3093
3362
|
return base + extra;
|
|
3094
3363
|
}
|
|
3095
3364
|
|
|
3365
|
+
function getSuggestedSpeseConvivenza() {
|
|
3366
|
+
const suggested = sumSpese("c1") + sumSpese("c2");
|
|
3367
|
+
return Math.max(0, Math.round(Number(suggested || 0)));
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
function maybeAutoFillSpeseConvivenza() {
|
|
3371
|
+
if (!speseConvivenzaAutoMode) return;
|
|
3372
|
+
const input = document.getElementById("speseConvivenza");
|
|
3373
|
+
if (!input) return;
|
|
3374
|
+
|
|
3375
|
+
const current = num("speseConvivenza");
|
|
3376
|
+
if (current > 0.005) {
|
|
3377
|
+
speseConvivenzaAutoMode = false;
|
|
3378
|
+
return;
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
const suggested = getSuggestedSpeseConvivenza();
|
|
3382
|
+
if (suggested <= 0) return;
|
|
3383
|
+
input.value = String(suggested);
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3096
3386
|
/**
|
|
3097
3387
|
* Stima il netto mensile da reddito lordo annuale (Certificazione Unica).
|
|
3098
3388
|
* Applica: contributi INPS dipendente (9,19%), IRPEF 2025, detrazioni da
|
|
@@ -3311,6 +3601,7 @@ const defaultExpenseItems = [
|
|
|
3311
3601
|
aFam2: num("assegnoFam2"),
|
|
3312
3602
|
primaCasaMutuoEnabled: firstHome.enabled ? 1 : 0,
|
|
3313
3603
|
primaCasaMutuoImporto: firstHome.amount,
|
|
3604
|
+
primaCasaValoreLocativo: firstHome.rentalValue,
|
|
3314
3605
|
primaCasaAssegnataA: firstHome.assignedTo,
|
|
3315
3606
|
primaCasaMutuoPerc1: firstHome.share1,
|
|
3316
3607
|
straordAnn1: num("straordAnn1"),
|
|
@@ -3352,6 +3643,7 @@ const defaultExpenseItems = [
|
|
|
3352
3643
|
const aFam2 = Number(payload.aFam2 || 0);
|
|
3353
3644
|
const primaCasaMutuoEnabled = Number(payload.primaCasaMutuoEnabled || 0) > 0;
|
|
3354
3645
|
const primaCasaMutuoImporto = Math.max(0, Number(payload.primaCasaMutuoImporto || 0));
|
|
3646
|
+
const primaCasaValoreLocativo = Math.max(0, Number(payload.primaCasaValoreLocativo || 0));
|
|
3355
3647
|
const primaCasaAssegnataA = (String(payload.primaCasaAssegnataA || "") === "1" || String(payload.primaCasaAssegnataA || "") === "2")
|
|
3356
3648
|
? String(payload.primaCasaAssegnataA)
|
|
3357
3649
|
: "";
|
|
@@ -3413,9 +3705,10 @@ const defaultExpenseItems = [
|
|
|
3413
3705
|
const assegnoBaseDa1a2 = assegnoDa1a2;
|
|
3414
3706
|
const assegnoBaseDa2a1 = assegnoDa2a1;
|
|
3415
3707
|
|
|
3416
|
-
const
|
|
3708
|
+
const primaCasaAssignmentConsidered = primaCasaMutuoEnabled
|
|
3417
3709
|
&& primaCasaAssegnataA !== ""
|
|
3418
3710
|
&& Number(primaCasaAssegnataA) === collocatario;
|
|
3711
|
+
const primaCasaConsidered = primaCasaAssignmentConsidered && primaCasaMutuoImporto > 0;
|
|
3419
3712
|
let primaCasaTransfer1to2 = 0;
|
|
3420
3713
|
let primaCasaTransfer2to1 = 0;
|
|
3421
3714
|
if (primaCasaConsidered) {
|
|
@@ -3426,14 +3719,27 @@ const defaultExpenseItems = [
|
|
|
3426
3719
|
}
|
|
3427
3720
|
}
|
|
3428
3721
|
|
|
3429
|
-
|
|
3430
|
-
|
|
3722
|
+
const primaCasaLocativeConsidered = primaCasaAssignmentConsidered && primaCasaValoreLocativo > 0;
|
|
3723
|
+
let primaCasaLocativeTransfer1to2 = 0;
|
|
3724
|
+
let primaCasaLocativeTransfer2to1 = 0;
|
|
3725
|
+
if (primaCasaLocativeConsidered) {
|
|
3726
|
+
if (primaCasaAssegnataA === "1") {
|
|
3727
|
+
primaCasaLocativeTransfer2to1 = primaCasaValoreLocativo;
|
|
3728
|
+
} else if (primaCasaAssegnataA === "2") {
|
|
3729
|
+
primaCasaLocativeTransfer1to2 = primaCasaValoreLocativo;
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
assegnoDa1a2 = Math.max(0, assegnoDa1a2 - primaCasaTransfer1to2 - primaCasaLocativeTransfer1to2);
|
|
3734
|
+
assegnoDa2a1 = Math.max(0, assegnoDa2a1 - primaCasaTransfer2to1 - primaCasaLocativeTransfer2to1);
|
|
3431
3735
|
|
|
3432
3736
|
const compensativeBenefits = [];
|
|
3433
3737
|
if (aFam1 > 0.005) compensativeBenefits.push({ type: "family", to: 1, amount: aFam1 });
|
|
3434
3738
|
if (aFam2 > 0.005) compensativeBenefits.push({ type: "family", to: 2, amount: aFam2 });
|
|
3435
3739
|
if (primaCasaTransfer1to2 > 0.005) compensativeBenefits.push({ type: "primary-home-mortgage", from: 1, to: 2, amount: primaCasaTransfer1to2 });
|
|
3436
3740
|
if (primaCasaTransfer2to1 > 0.005) compensativeBenefits.push({ type: "primary-home-mortgage", from: 2, to: 1, amount: primaCasaTransfer2to1 });
|
|
3741
|
+
if (primaCasaLocativeTransfer1to2 > 0.005) compensativeBenefits.push({ type: "primary-home-assignment", from: 1, to: 2, amount: primaCasaLocativeTransfer1to2 });
|
|
3742
|
+
if (primaCasaLocativeTransfer2to1 > 0.005) compensativeBenefits.push({ type: "primary-home-assignment", from: 2, to: 1, amount: primaCasaLocativeTransfer2to1 });
|
|
3437
3743
|
|
|
3438
3744
|
const post1 = disp1 - assegnoDa1a2 + assegnoDa2a1;
|
|
3439
3745
|
const post2 = disp2 - assegnoDa2a1 + assegnoDa1a2;
|
|
@@ -3489,9 +3795,10 @@ const defaultExpenseItems = [
|
|
|
3489
3795
|
quotaDiretta1, quotaDiretta2,
|
|
3490
3796
|
saldo1, saldo2,
|
|
3491
3797
|
assegnoBaseDa1a2, assegnoBaseDa2a1,
|
|
3492
|
-
primaCasaMutuoEnabled, primaCasaMutuoImporto, primaCasaAssegnataA,
|
|
3798
|
+
primaCasaMutuoEnabled, primaCasaMutuoImporto, primaCasaValoreLocativo, primaCasaAssegnataA,
|
|
3493
3799
|
primaCasaMutuoPerc1, primaCasaMutuoPerc2,
|
|
3494
|
-
primaCasaConsidered, primaCasaTransfer1to2, primaCasaTransfer2to1,
|
|
3800
|
+
primaCasaAssignmentConsidered, primaCasaConsidered, primaCasaTransfer1to2, primaCasaTransfer2to1,
|
|
3801
|
+
primaCasaLocativeConsidered, primaCasaLocativeTransfer1to2, primaCasaLocativeTransfer2to1,
|
|
3495
3802
|
compensativeBenefits,
|
|
3496
3803
|
assegnoDa1a2, assegnoDa2a1,
|
|
3497
3804
|
post1, post2,
|
|
@@ -3593,16 +3900,18 @@ const defaultExpenseItems = [
|
|
|
3593
3900
|
function getFirstHomeMortgageInput() {
|
|
3594
3901
|
const enabled = !!document.getElementById("primaCasaMutuoEnabled")?.checked;
|
|
3595
3902
|
const amount = Math.max(0, num("primaCasaMutuoImporto"));
|
|
3903
|
+
const rentalValue = Math.max(0, num("primaCasaValoreLocativo"));
|
|
3596
3904
|
const assignedToRaw = String(document.getElementById("primaCasaAssegnataA")?.value || "").trim();
|
|
3597
3905
|
const assignedTo = (assignedToRaw === "1" || assignedToRaw === "2") ? assignedToRaw : "";
|
|
3598
3906
|
const share1 = Math.min(100, Math.max(0, num("primaCasaMutuoPerc1")));
|
|
3599
3907
|
const share2 = 100 - share1;
|
|
3600
|
-
return { enabled, amount, assignedTo, share1, share2 };
|
|
3908
|
+
return { enabled, amount, rentalValue, assignedTo, share1, share2 };
|
|
3601
3909
|
}
|
|
3602
3910
|
|
|
3603
3911
|
function updateFirstHomeMortgageUi() {
|
|
3604
3912
|
const enabledEl = document.getElementById("primaCasaMutuoEnabled");
|
|
3605
3913
|
const amountEl = document.getElementById("primaCasaMutuoImporto");
|
|
3914
|
+
const rentalValueEl = document.getElementById("primaCasaValoreLocativo");
|
|
3606
3915
|
const assignedEl = document.getElementById("primaCasaAssegnataA");
|
|
3607
3916
|
const shareEl = document.getElementById("primaCasaMutuoPerc1");
|
|
3608
3917
|
const splitInfoEl = document.getElementById("primaCasaMutuoSplitInfo");
|
|
@@ -3618,6 +3927,7 @@ const defaultExpenseItems = [
|
|
|
3618
3927
|
|
|
3619
3928
|
const isEnabled = !!enabledEl.checked;
|
|
3620
3929
|
amountEl.disabled = !isEnabled;
|
|
3930
|
+
if (rentalValueEl) rentalValueEl.disabled = !isEnabled;
|
|
3621
3931
|
assignedEl.disabled = !isEnabled;
|
|
3622
3932
|
shareEl.disabled = !isEnabled;
|
|
3623
3933
|
if (splitWrapEl) splitWrapEl.classList.toggle("is-disabled", !isEnabled);
|
|
@@ -3642,6 +3952,8 @@ const defaultExpenseItems = [
|
|
|
3642
3952
|
});
|
|
3643
3953
|
}
|
|
3644
3954
|
if (splitCenterEl) splitCenterEl.textContent = `${normalizedShare1.toFixed(0)}% / ${share2.toFixed(0)}%`;
|
|
3955
|
+
const splitRangeWrapEl = shareEl.closest(".mortgage-split-range-wrap");
|
|
3956
|
+
if (splitRangeWrapEl) splitRangeWrapEl.style.setProperty("--split-left", `${normalizedShare1.toFixed(0)}%`);
|
|
3645
3957
|
if (splitLeftNameEl) splitLeftNameEl.textContent = c1n();
|
|
3646
3958
|
if (splitRightNameEl) splitRightNameEl.textContent = c2n();
|
|
3647
3959
|
if (splitLeftAmountEl) splitLeftAmountEl.textContent = eur(quota1);
|
|
@@ -4333,6 +4645,29 @@ const defaultExpenseItems = [
|
|
|
4333
4645
|
return `<span class="spieg-help-wrap"><button type="button" class="spieg-help-btn" aria-label="${tooltipLabel}">i</button><span class="spieg-help-tip">${safeText}</span></span>`;
|
|
4334
4646
|
};
|
|
4335
4647
|
|
|
4648
|
+
let benefitSectionHtml = "";
|
|
4649
|
+
if (compBenefits.length) {
|
|
4650
|
+
const rawBenefs = Array.isArray(m.compensativeBenefits)
|
|
4651
|
+
? m.compensativeBenefits.filter((r) => r && Number(r.amount || 0) > 0.005)
|
|
4652
|
+
: [];
|
|
4653
|
+
const typeIcons = { family: "\uD83C\uDFDB", "primary-home-mortgage": "\uD83C\uDFE1", "primary-home-assignment": "\uD83C\uDFE0" };
|
|
4654
|
+
const cardsHtml = compBenefits
|
|
4655
|
+
.map((row, i) => {
|
|
4656
|
+
const icon = (rawBenefs[i] && typeIcons[rawBenefs[i].type]) || "\u2726";
|
|
4657
|
+
return `<li class="spieg-benefit-card"><span class="spieg-benefit-icon">${icon}</span><span class="spieg-benefit-label">${escapeHtml(row.label)}</span><strong class="spieg-benefit-amount">${eur(row.amount)}</strong></li>`;
|
|
4658
|
+
})
|
|
4659
|
+
.join("");
|
|
4660
|
+
const total = compBenefits.reduce((s, r) => s + r.amount, 0);
|
|
4661
|
+
const totalLabel = currentLang === "en" ? "Total allocated benefits" : "Totale benefici allocati";
|
|
4662
|
+
benefitSectionHtml = `
|
|
4663
|
+
<div class="spieg-benefits-section">
|
|
4664
|
+
<div class="spieg-benefits-label">🎁 ${escapeHtml(tr("calcCompBenefitsLabel"))}</div>
|
|
4665
|
+
<ul class="spieg-benefits-cards">${cardsHtml}</ul>
|
|
4666
|
+
<div class="spieg-benefits-total"><span>${escapeHtml(totalLabel)}</span><strong>${eur(total)}</strong></div>
|
|
4667
|
+
</div>
|
|
4668
|
+
`;
|
|
4669
|
+
}
|
|
4670
|
+
|
|
4336
4671
|
let resultHtml;
|
|
4337
4672
|
let resultDetail;
|
|
4338
4673
|
if (isAssegno1) {
|
|
@@ -4340,6 +4675,7 @@ const defaultExpenseItems = [
|
|
|
4340
4675
|
<div class="spieg-result-flow">${n1} → ${n2}</div>
|
|
4341
4676
|
<div class="spieg-result-formula">${n1}: ${eur(m.quotaTeorica1)} − ${eur(m.quotaDiretta1)}</div>
|
|
4342
4677
|
<div class="spieg-result-amount ok">${eur(m.assegnoDa1a2)}</div>
|
|
4678
|
+
${benefitSectionHtml}
|
|
4343
4679
|
`;
|
|
4344
4680
|
resultDetail = tr("spiegDetailResultTransfer");
|
|
4345
4681
|
} else if (isAssegno2) {
|
|
@@ -4347,30 +4683,14 @@ const defaultExpenseItems = [
|
|
|
4347
4683
|
<div class="spieg-result-flow">${n2} → ${n1}</div>
|
|
4348
4684
|
<div class="spieg-result-formula">${n2}: ${eur(m.quotaTeorica2)} − ${eur(m.quotaDiretta2)}</div>
|
|
4349
4685
|
<div class="spieg-result-amount ok">${eur(m.assegnoDa2a1)}</div>
|
|
4686
|
+
${benefitSectionHtml}
|
|
4350
4687
|
`;
|
|
4351
4688
|
resultDetail = tr("spiegDetailResultTransfer");
|
|
4352
4689
|
} else {
|
|
4353
|
-
|
|
4354
|
-
if (benefitRows.length) {
|
|
4355
|
-
const rawBenefs = Array.isArray(m.compensativeBenefits)
|
|
4356
|
-
? m.compensativeBenefits.filter((r) => r && Number(r.amount || 0) > 0.005)
|
|
4357
|
-
: [];
|
|
4358
|
-
const typeIcons = { family: "\uD83C\uDFDB", "primary-home-mortgage": "\uD83C\uDFE1" };
|
|
4359
|
-
const cardsHtml = benefitRows
|
|
4360
|
-
.map((row, i) => {
|
|
4361
|
-
const icon = (rawBenefs[i] && typeIcons[rawBenefs[i].type]) || "\u2726";
|
|
4362
|
-
return `<li class="spieg-benefit-card"><span class="spieg-benefit-icon">${icon}</span><span class="spieg-benefit-label">${escapeHtml(row.label)}</span><strong class="spieg-benefit-amount">${eur(row.amount)}</strong></li>`;
|
|
4363
|
-
})
|
|
4364
|
-
.join("");
|
|
4365
|
-
const total = benefitRows.reduce((s, r) => s + r.amount, 0);
|
|
4366
|
-
const totalLabel = currentLang === "en" ? "Total allocated benefits" : "Totale benefici allocati";
|
|
4690
|
+
if (benefitSectionHtml) {
|
|
4367
4691
|
resultHtml = `
|
|
4368
4692
|
<div class="spieg-no-transfer-badge">⚖️ ${escapeHtml(tr("calcNoTransferSuggested"))}</div>
|
|
4369
|
-
|
|
4370
|
-
<div class="spieg-benefits-label">🎁 ${escapeHtml(tr("calcCompBenefitsLabel"))}</div>
|
|
4371
|
-
<ul class="spieg-benefits-cards">${cardsHtml}</ul>
|
|
4372
|
-
<div class="spieg-benefits-total"><span>${escapeHtml(totalLabel)}</span><strong>${eur(total)}</strong></div>
|
|
4373
|
-
</div>
|
|
4693
|
+
${benefitSectionHtml}
|
|
4374
4694
|
`;
|
|
4375
4695
|
} else {
|
|
4376
4696
|
resultHtml = `<div class="spieg-result-empty ok">${tr("calcNoTransferSuggested")}</div>`;
|
|
@@ -4454,6 +4774,11 @@ const defaultExpenseItems = [
|
|
|
4454
4774
|
const receiver = Number(row.to) === 2 ? name2 : name1;
|
|
4455
4775
|
return { label: msg("calcBenefitPrimaryHomeMortgage", { payer, receiver }), amount };
|
|
4456
4776
|
}
|
|
4777
|
+
if (row.type === "primary-home-assignment") {
|
|
4778
|
+
const payer = Number(row.from) === 2 ? name2 : name1;
|
|
4779
|
+
const receiver = Number(row.to) === 2 ? name2 : name1;
|
|
4780
|
+
return { label: msg("calcBenefitPrimaryHomeAssignment", { payer, receiver }), amount };
|
|
4781
|
+
}
|
|
4457
4782
|
return { label: tr("calcCompBenefitsLabel"), amount };
|
|
4458
4783
|
});
|
|
4459
4784
|
}
|
|
@@ -4534,6 +4859,30 @@ const defaultExpenseItems = [
|
|
|
4534
4859
|
${m.incomeMode === "cu" ? `<br /><strong>${tr("calcIncomeBaseNote")}</strong> ${tr("cuNetNoteText")}` : ""}
|
|
4535
4860
|
`;
|
|
4536
4861
|
|
|
4862
|
+
const benefitRows = getCompensativeBenefitRows(m, c1n(), c2n());
|
|
4863
|
+
let benefitCardsHtml = "";
|
|
4864
|
+
if (benefitRows.length) {
|
|
4865
|
+
const rawBenefs = Array.isArray(m.compensativeBenefits)
|
|
4866
|
+
? m.compensativeBenefits.filter((r) => r && Number(r.amount || 0) > 0.005)
|
|
4867
|
+
: [];
|
|
4868
|
+
const typeIcons = { family: "\uD83C\uDFDB", "primary-home-mortgage": "\uD83C\uDFE1", "primary-home-assignment": "\uD83C\uDFE0" };
|
|
4869
|
+
const cardsHtml = benefitRows
|
|
4870
|
+
.map((row, i) => {
|
|
4871
|
+
const icon = (rawBenefs[i] && typeIcons[rawBenefs[i].type]) || "\u2726";
|
|
4872
|
+
return `<li class="spieg-benefit-card"><span class="spieg-benefit-icon">${icon}</span><span class="spieg-benefit-label">${escapeHtml(row.label)}</span><strong class="spieg-benefit-amount">${eur(row.amount)}</strong></li>`;
|
|
4873
|
+
})
|
|
4874
|
+
.join("");
|
|
4875
|
+
const total = benefitRows.reduce((s, r) => s + r.amount, 0);
|
|
4876
|
+
const totalLabel = currentLang === "en" ? "Total allocated benefits" : "Totale benefici allocati";
|
|
4877
|
+
benefitCardsHtml = `
|
|
4878
|
+
<div class="result-benefits-box">
|
|
4879
|
+
<div class="spieg-benefits-label">🎁 ${escapeHtml(tr("calcCompBenefitsLabel"))}</div>
|
|
4880
|
+
<ul class="spieg-benefits-cards">${cardsHtml}</ul>
|
|
4881
|
+
<div class="spieg-benefits-total"><span>${escapeHtml(totalLabel)}</span><strong>${eur(total)}</strong></div>
|
|
4882
|
+
</div>
|
|
4883
|
+
`;
|
|
4884
|
+
}
|
|
4885
|
+
|
|
4537
4886
|
let mainHtml;
|
|
4538
4887
|
if (m.assegnoDa1a2 > 0.005) {
|
|
4539
4888
|
const perChild = m.figli > 1 ? `<div class="result-transfer-child">${eur(m.assegnoDa1a2 / m.figli)} ${escapeHtml(currentLang === "en" ? "per child" : "per figlio")}</div>` : "";
|
|
@@ -4545,6 +4894,7 @@ const defaultExpenseItems = [
|
|
|
4545
4894
|
</div>
|
|
4546
4895
|
<div class="result-transfer-value">${eur(m.assegnoDa1a2)}<span class="result-transfer-per"> ${escapeHtml(tr("pdfPerMonth"))}</span></div>
|
|
4547
4896
|
${perChild}
|
|
4897
|
+
${benefitCardsHtml}
|
|
4548
4898
|
`;
|
|
4549
4899
|
} else if (m.assegnoDa2a1 > 0.005) {
|
|
4550
4900
|
const perChild = m.figli > 1 ? `<div class="result-transfer-child">${eur(m.assegnoDa2a1 / m.figli)} ${escapeHtml(currentLang === "en" ? "per child" : "per figlio")}</div>` : "";
|
|
@@ -4556,31 +4906,9 @@ const defaultExpenseItems = [
|
|
|
4556
4906
|
</div>
|
|
4557
4907
|
<div class="result-transfer-value">${eur(m.assegnoDa2a1)}<span class="result-transfer-per"> ${escapeHtml(tr("pdfPerMonth"))}</span></div>
|
|
4558
4908
|
${perChild}
|
|
4909
|
+
${benefitCardsHtml}
|
|
4559
4910
|
`;
|
|
4560
4911
|
} else {
|
|
4561
|
-
const benefitRows = getCompensativeBenefitRows(m, c1n(), c2n());
|
|
4562
|
-
let benefitCardsHtml = "";
|
|
4563
|
-
if (benefitRows.length) {
|
|
4564
|
-
const rawBenefs = Array.isArray(m.compensativeBenefits)
|
|
4565
|
-
? m.compensativeBenefits.filter((r) => r && Number(r.amount || 0) > 0.005)
|
|
4566
|
-
: [];
|
|
4567
|
-
const typeIcons = { family: "\uD83C\uDFDB", "primary-home-mortgage": "\uD83C\uDFE1" };
|
|
4568
|
-
const cardsHtml = benefitRows
|
|
4569
|
-
.map((row, i) => {
|
|
4570
|
-
const icon = (rawBenefs[i] && typeIcons[rawBenefs[i].type]) || "\u2726";
|
|
4571
|
-
return `<li class="spieg-benefit-card"><span class="spieg-benefit-icon">${icon}</span><span class="spieg-benefit-label">${escapeHtml(row.label)}</span><strong class="spieg-benefit-amount">${eur(row.amount)}</strong></li>`;
|
|
4572
|
-
})
|
|
4573
|
-
.join("");
|
|
4574
|
-
const total = benefitRows.reduce((s, r) => s + r.amount, 0);
|
|
4575
|
-
const totalLabel = currentLang === "en" ? "Total allocated benefits" : "Totale benefici allocati";
|
|
4576
|
-
benefitCardsHtml = `
|
|
4577
|
-
<div class="result-benefits-box">
|
|
4578
|
-
<div class="spieg-benefits-label">🎁 ${escapeHtml(tr("calcCompBenefitsLabel"))}</div>
|
|
4579
|
-
<ul class="spieg-benefits-cards">${cardsHtml}</ul>
|
|
4580
|
-
<div class="spieg-benefits-total"><span>${escapeHtml(totalLabel)}</span><strong>${eur(total)}</strong></div>
|
|
4581
|
-
</div>
|
|
4582
|
-
`;
|
|
4583
|
-
}
|
|
4584
4912
|
mainHtml = `<div class="spieg-no-transfer-badge">⚖️ ${escapeHtml(tr("calcNoTransferSuggested"))}</div>${benefitCardsHtml}`;
|
|
4585
4913
|
}
|
|
4586
4914
|
resultMain.innerHTML = mainHtml;
|
|
@@ -4656,6 +4984,7 @@ const defaultExpenseItems = [
|
|
|
4656
4984
|
if (!input) return;
|
|
4657
4985
|
const suggested = Math.max(0, Math.round(Number(m.speseTot || 0)));
|
|
4658
4986
|
input.value = String(suggested);
|
|
4987
|
+
speseConvivenzaAutoMode = false;
|
|
4659
4988
|
input.focus();
|
|
4660
4989
|
renderAll();
|
|
4661
4990
|
});
|
|
@@ -4719,6 +5048,7 @@ const defaultExpenseItems = [
|
|
|
4719
5048
|
}
|
|
4720
5049
|
|
|
4721
5050
|
function renderAll() {
|
|
5051
|
+
maybeAutoFillSpeseConvivenza();
|
|
4722
5052
|
const m = computeModel();
|
|
4723
5053
|
updateExtraordinaryModuleUi();
|
|
4724
5054
|
updateExpensePartials();
|
|
@@ -4812,27 +5142,49 @@ const defaultExpenseItems = [
|
|
|
4812
5142
|
? `
|
|
4813
5143
|
<tr><td>${tr("pdfPrimaryHomeAssignedTo")}</td><td>${primaryHomeAssignedLabel}</td></tr>
|
|
4814
5144
|
<tr><td>${tr("pdfPrimaryHomeMonthlyAmount")}</td><td>${eur(m.primaCasaMutuoImporto || 0)}</td></tr>
|
|
5145
|
+
<tr><td>${tr("pdfPrimaryHomeRentalValue")}</td><td>${eur(m.primaCasaValoreLocativo || 0)}</td></tr>
|
|
4815
5146
|
<tr><td>${tr("pdfPrimaryHomeSplit")}</td><td>${c1NameEsc} ${(m.primaCasaMutuoPerc1 || 0).toFixed(0)}% Β· ${c2NameEsc} ${(m.primaCasaMutuoPerc2 || 0).toFixed(0)}%</td></tr>
|
|
4816
5147
|
<tr><td>${tr("pdfPrimaryHomeAppliedOnlyColl")}</td><td>${m.primaCasaConsidered ? "OK" : tr("pdfPrimaryHomeNotDeclared")}</td></tr>`
|
|
4817
5148
|
: `<tr><td>${tr("pdfPrimaryHomeMortgage")}</td><td>${tr("pdfPrimaryHomeNotDeclared")}</td></tr>`;
|
|
4818
5149
|
|
|
4819
5150
|
let explainResultHtml = `<div class="pdf-explain-result-empty">${tr("calcNoTransferSuggested")}</div>`;
|
|
5151
|
+
let pdfBenefitSectionHtml = "";
|
|
5152
|
+
if (compBenefits.length) {
|
|
5153
|
+
const rawBenefs = Array.isArray(m.compensativeBenefits)
|
|
5154
|
+
? m.compensativeBenefits.filter((row) => row && Number(row.amount || 0) > 0.005)
|
|
5155
|
+
: [];
|
|
5156
|
+
const typeIcons = { family: "\uD83C\uDFDB", "primary-home-mortgage": "\uD83C\uDFE1", "primary-home-assignment": "\uD83C\uDFE0" };
|
|
5157
|
+
const cardsHtml = compBenefits.map((row, idx) => {
|
|
5158
|
+
const icon = (rawBenefs[idx] && typeIcons[rawBenefs[idx].type]) || "\u2726";
|
|
5159
|
+
return `<li class="pdf-explain-benefit-card"><span class="pdf-explain-benefit-icon">${icon}</span><span class="pdf-explain-benefit-label">${escapeHtml(row.label)}</span><strong class="pdf-explain-benefit-amount">${eur(row.amount)}</strong></li>`;
|
|
5160
|
+
}).join("");
|
|
5161
|
+
const benefitsTotal = compBenefits.reduce((sum, row) => sum + Number(row.amount || 0), 0);
|
|
5162
|
+
pdfBenefitSectionHtml = `
|
|
5163
|
+
<div class="pdf-explain-benefits-section">
|
|
5164
|
+
<div class="pdf-explain-benefits-label">🎁 ${escapeHtml(tr("calcCompBenefitsLabel"))}</div>
|
|
5165
|
+
<ul class="pdf-explain-benefits-cards">${cardsHtml}</ul>
|
|
5166
|
+
<div class="pdf-explain-benefits-total"><span>${escapeHtml(tr("pdfCompBenefitsTotal"))}</span><strong>${eur(benefitsTotal)}</strong></div>
|
|
5167
|
+
</div>
|
|
5168
|
+
`;
|
|
5169
|
+
}
|
|
4820
5170
|
if (m.assegnoDa1a2 > 0.005) {
|
|
4821
5171
|
explainResultHtml = `
|
|
4822
5172
|
<div class="pdf-explain-flow">${c1NameEsc} → ${c2NameEsc}</div>
|
|
4823
5173
|
<div class="pdf-explain-formula">${c1NameEsc}: ${eur(m.quotaTeorica1)} − ${eur(m.quotaDiretta1)}</div>
|
|
4824
5174
|
<div class="pdf-explain-amount">${eur(m.assegnoDa1a2)}</div>
|
|
5175
|
+
${pdfBenefitSectionHtml}
|
|
4825
5176
|
`;
|
|
4826
5177
|
} else if (m.assegnoDa2a1 > 0.005) {
|
|
4827
5178
|
explainResultHtml = `
|
|
4828
5179
|
<div class="pdf-explain-flow">${c2NameEsc} → ${c1NameEsc}</div>
|
|
4829
5180
|
<div class="pdf-explain-formula">${c2NameEsc}: ${eur(m.quotaTeorica2)} − ${eur(m.quotaDiretta2)}</div>
|
|
4830
5181
|
<div class="pdf-explain-amount">${eur(m.assegnoDa2a1)}</div>
|
|
5182
|
+
${pdfBenefitSectionHtml}
|
|
4831
5183
|
`;
|
|
4832
5184
|
} else if (compBenefits.length) {
|
|
4833
5185
|
explainResultHtml = `
|
|
4834
|
-
<div class="pdf-explain-
|
|
4835
|
-
|
|
5186
|
+
<div class="pdf-explain-no-transfer-badge">⚖️ ${escapeHtml(tr("calcNoTransferSuggested"))}</div>
|
|
5187
|
+
${pdfBenefitSectionHtml}
|
|
4836
5188
|
`;
|
|
4837
5189
|
}
|
|
4838
5190
|
|
|
@@ -5002,30 +5354,20 @@ const defaultExpenseItems = [
|
|
|
5002
5354
|
</div>`;
|
|
5003
5355
|
}).join("")}</div>`
|
|
5004
5356
|
: "";
|
|
5005
|
-
const
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
<div class="section-title">${tr("sepCostPanelTitle")}</div>
|
|
5009
|
-
<table>
|
|
5010
|
-
<tbody>
|
|
5011
|
-
<tr><td>${tr("sepCostNetTogether")}</td><td class="num"><strong>${eur(m.nettoInsiemeCombinato || 0)}</strong></td></tr>
|
|
5012
|
-
<tr><td>${tr("sepCostNetSeparated")}</td><td class="num"><strong>${eur(m.nettoSeparatoTotale || 0)}</strong></td></tr>
|
|
5013
|
-
${m.separationAdjustmentHousingUtilities > 0 ? `<tr><td>${tr("sepCostHousingUtilityAdj")}</td><td class="num">${eur(m.separationAdjustmentHousingUtilities)}</td></tr>` : ""}
|
|
5014
|
-
<tr><td>${tr("sepCostDuplication")}</td><td class="num"><strong>${eur(m.costoSeparazioneMensile || 0)}</strong></td></tr>
|
|
5015
|
-
<tr><td>${tr("sepCostLossMonthly")}</td><td class="num"><strong>${eur(m.perditaMensile || 0)}</strong></td></tr>
|
|
5016
|
-
<tr><td>${tr("sepCostLossAnnually")}</td><td class="num"><strong>${eur(m.perditaAnnua || 0)}</strong></td></tr>
|
|
5017
|
-
<tr><td>${msg("sepCostLossSpouse", { spouse: c1NameEsc })}</td><td class="num">${eur(m.perditaSpouse1 || 0)}</td></tr>
|
|
5018
|
-
<tr><td>${msg("sepCostLossSpouse", { spouse: c2NameEsc })}</td><td class="num">${eur(m.perditaSpouse2 || 0)}</td></tr>
|
|
5019
|
-
</tbody>
|
|
5020
|
-
</table>
|
|
5021
|
-
</div>`
|
|
5357
|
+
const liveSepPanelSnapshot = String(document.getElementById("sepCostPanel")?.innerHTML || "").trim();
|
|
5358
|
+
const separationSectionHtml = m.speseConvivenza > 0 && liveSepPanelSnapshot
|
|
5359
|
+
? `<div class="section pdf-screen-section">${liveSepPanelSnapshot}</div>`
|
|
5022
5360
|
: "";
|
|
5361
|
+
const activeAppScriptSrc = String(document.querySelector('script[src*="app.js"]')?.getAttribute("src") || "");
|
|
5362
|
+
const appVersionFromScript = (activeAppScriptSrc.match(/[?&]v=([^&#]+)/) || [])[1] || String(Date.now());
|
|
5363
|
+
const pdfStylesHref = `styles.css?v=${encodeURIComponent(appVersionFromScript)}`;
|
|
5023
5364
|
|
|
5024
5365
|
const html = `<!DOCTYPE html>
|
|
5025
5366
|
<html lang="${pdfLang}">
|
|
5026
5367
|
<head>
|
|
5027
5368
|
<meta charset="UTF-8"/>
|
|
5028
5369
|
<title>${tr("pdfReportTitle")} β ${genDate}</title>
|
|
5370
|
+
<link rel="stylesheet" href="${pdfStylesHref}">
|
|
5029
5371
|
<style>
|
|
5030
5372
|
@page { size: A4 portrait; margin: 16mm 14mm 14mm 14mm; }
|
|
5031
5373
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
@@ -5196,6 +5538,16 @@ const defaultExpenseItems = [
|
|
|
5196
5538
|
.pdf-explain-formula { font-size: 8.5pt; color: #284e49; margin-bottom: 3px; }
|
|
5197
5539
|
.pdf-explain-amount { font-size: 13pt; font-weight: 900; color: #0e6b62; }
|
|
5198
5540
|
.pdf-explain-result-empty { font-size: 10pt; font-weight: 800; color: #0f6a61; }
|
|
5541
|
+
.pdf-explain-no-transfer-badge { display: block; border: 1px solid #93d1bc; background: linear-gradient(90deg,#e7f6ef,#cdeedf); border-radius: 999px; padding: 8px 12px; text-align: center; font-size: 10pt; font-weight: 800; color: #0f6158; margin-bottom: 10px; }
|
|
5542
|
+
.pdf-explain-benefits-section { border: 1px solid #b9ddd1; border-radius: 10px; background: #f8fcfb; padding: 8px 10px; }
|
|
5543
|
+
.pdf-explain-benefits-label { font-size: 8pt; font-weight: 800; color: #1d5550; text-transform: uppercase; letter-spacing: 0.45px; margin-bottom: 7px; }
|
|
5544
|
+
.pdf-explain-benefits-cards { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 7px; }
|
|
5545
|
+
.pdf-explain-benefit-card { display: flex; align-items: center; gap: 8px; border: 1px solid #c7e2db; border-radius: 9px; background: #fff; padding: 7px 9px; }
|
|
5546
|
+
.pdf-explain-benefit-icon { width: 22px; height: 22px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; background: #def0ea; border: 1px solid #bddbd2; font-size: 10pt; }
|
|
5547
|
+
.pdf-explain-benefit-label { flex: 1; font-size: 8.4pt; color: #214b46; font-weight: 700; }
|
|
5548
|
+
.pdf-explain-benefit-amount { font-size: 10pt; color: #0e645b; font-weight: 900; }
|
|
5549
|
+
.pdf-explain-benefits-total { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 8px; padding: 8px 10px; border-radius: 9px; background: linear-gradient(90deg,#dff3ea,#bfe8d8); border: 1px solid #9cd0bb; color: #124d46; font-size: 8.6pt; font-weight: 700; }
|
|
5550
|
+
.pdf-explain-benefits-total strong { font-size: 12pt; color: #0b5d54; }
|
|
5199
5551
|
|
|
5200
5552
|
/* ββ PDF INLINE CALENDAR ββ */
|
|
5201
5553
|
.pdf-cal-wrap { margin-top: 8px; }
|
|
@@ -5216,6 +5568,14 @@ const defaultExpenseItems = [
|
|
|
5216
5568
|
.footer { border-top: 1px solid #cde5e0; padding-top: 6px; margin-top: 20px;
|
|
5217
5569
|
font-size: 7.5pt; color: #888; text-align: center; }
|
|
5218
5570
|
|
|
5571
|
+
/* Keep on-screen section visuals (e.g. separation panel) close to app rendering */
|
|
5572
|
+
.pdf-screen-section { break-inside: avoid; }
|
|
5573
|
+
.pdf-screen-section .sep-cost-panel { margin-top: 0; }
|
|
5574
|
+
.pdf-screen-section .sep-cost-title { margin-bottom: 6px; }
|
|
5575
|
+
.pdf-screen-section .sep-cost-hero,
|
|
5576
|
+
.pdf-screen-section .sep-cost-section--grid,
|
|
5577
|
+
.pdf-screen-section .sep-cost-pill-wrap { gap: 8px; }
|
|
5578
|
+
|
|
5219
5579
|
@media print {
|
|
5220
5580
|
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
5221
5581
|
}
|
|
@@ -5542,32 +5902,49 @@ ${scenarioLab.length ? `
|
|
|
5542
5902
|
win.document.close();
|
|
5543
5903
|
}
|
|
5544
5904
|
|
|
5545
|
-
async function exportJson() {
|
|
5905
|
+
async function exportJson(options = {}) {
|
|
5546
5906
|
if (!authSession.username || !authSession.keyBits) {
|
|
5547
5907
|
setAuthStatus(tr("authLoginBeforeExportStatus"), true);
|
|
5548
5908
|
alert(tr("authLoginBeforeExportAlert"));
|
|
5549
5909
|
return;
|
|
5550
5910
|
}
|
|
5551
5911
|
|
|
5912
|
+
const plain = !!options.plain;
|
|
5552
5913
|
const state = serializeState();
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5914
|
+
let payloadObj;
|
|
5915
|
+
if (plain) {
|
|
5916
|
+
payloadObj = {
|
|
5917
|
+
format: "keylock-plain-state-v1",
|
|
5918
|
+
owner: authSession.username,
|
|
5919
|
+
createdAt: new Date().toISOString(),
|
|
5920
|
+
state
|
|
5921
|
+
};
|
|
5922
|
+
} else {
|
|
5923
|
+
const encrypted = await encryptStateForKey(state, authSession.keyBits);
|
|
5924
|
+
payloadObj = {
|
|
5925
|
+
format: "keylock-encrypted-state-v1",
|
|
5926
|
+
owner: authSession.username,
|
|
5927
|
+
createdAt: new Date().toISOString(),
|
|
5928
|
+
cipher: encrypted
|
|
5929
|
+
};
|
|
5930
|
+
}
|
|
5560
5931
|
const payload = JSON.stringify(payloadObj, null, 2);
|
|
5561
5932
|
const blob = new Blob([payload], { type: "application/json" });
|
|
5562
5933
|
const url = URL.createObjectURL(blob);
|
|
5563
5934
|
const a = document.createElement("a");
|
|
5564
5935
|
const ts = new Date().toISOString().replace(/[:T]/g, "-").slice(0, 16);
|
|
5565
5936
|
a.href = url;
|
|
5566
|
-
a.download =
|
|
5937
|
+
a.download = plain
|
|
5938
|
+
? `mantenimento-plain-${authSession.username}-${ts}.json`
|
|
5939
|
+
: `mantenimento-cifrato-${authSession.username}-${ts}.json`;
|
|
5567
5940
|
document.body.appendChild(a);
|
|
5568
5941
|
a.click();
|
|
5569
5942
|
a.remove();
|
|
5570
5943
|
URL.revokeObjectURL(url);
|
|
5944
|
+
|
|
5945
|
+
if (plain) {
|
|
5946
|
+
setAuthStatus(tr("authExportPlainFileWarning"), true);
|
|
5947
|
+
}
|
|
5571
5948
|
}
|
|
5572
5949
|
|
|
5573
5950
|
async function importJsonFromFile(file) {
|
|
@@ -5575,26 +5952,44 @@ ${scenarioLab.length ? `
|
|
|
5575
5952
|
reader.onload = async () => {
|
|
5576
5953
|
try {
|
|
5577
5954
|
const payload = JSON.parse(String(reader.result || "{}"));
|
|
5578
|
-
if (!payload || payload.format
|
|
5955
|
+
if (!payload || !payload.format || !payload.owner) {
|
|
5579
5956
|
throw new Error(tr("authInvalidJsonFormat"));
|
|
5580
5957
|
}
|
|
5581
5958
|
|
|
5582
|
-
if (!authSession.username
|
|
5959
|
+
if (!authSession.username) {
|
|
5583
5960
|
throw new Error(tr("authLoginBeforeImport"));
|
|
5584
5961
|
}
|
|
5585
5962
|
|
|
5586
|
-
|
|
5587
|
-
|
|
5963
|
+
let state = null;
|
|
5964
|
+
if (payload.format === "keylock-encrypted-state-v1") {
|
|
5965
|
+
if (!payload.cipher) {
|
|
5966
|
+
throw new Error(tr("authInvalidJsonFormat"));
|
|
5967
|
+
}
|
|
5968
|
+
if (!authSession.keyBits) {
|
|
5969
|
+
throw new Error(tr("authLoginBeforeImport"));
|
|
5970
|
+
}
|
|
5971
|
+
if (normalizeUsername(payload.owner) !== normalizeUsername(authSession.username)) {
|
|
5972
|
+
throw new Error(tr("authFileOwnedByOther"));
|
|
5973
|
+
}
|
|
5974
|
+
state = await decryptStateForKey(payload.cipher, authSession.keyBits);
|
|
5975
|
+
} else if (payload.format === "keylock-plain-state-v1") {
|
|
5976
|
+
state = payload.state;
|
|
5977
|
+
} else {
|
|
5978
|
+
throw new Error(tr("authInvalidJsonFormat"));
|
|
5588
5979
|
}
|
|
5589
5980
|
|
|
5590
|
-
const state = await decryptStateForKey(payload.cipher, authSession.keyBits);
|
|
5591
5981
|
if (!state || typeof state !== "object" || !state.base || !Array.isArray(state.spese)) {
|
|
5592
5982
|
throw new Error(tr("authDecryptedContentInvalid"));
|
|
5593
5983
|
}
|
|
5594
5984
|
|
|
5595
5985
|
applyState(state);
|
|
5596
|
-
|
|
5597
|
-
|
|
5986
|
+
if (payload.format === "keylock-plain-state-v1") {
|
|
5987
|
+
setAuthStatus(tr("authPlainJsonImported"));
|
|
5988
|
+
alert(tr("authPlainJsonLoaded"));
|
|
5989
|
+
} else {
|
|
5990
|
+
setAuthStatus(tr("authEncryptedJsonImported"));
|
|
5991
|
+
alert(tr("authEncryptedJsonLoaded"));
|
|
5992
|
+
}
|
|
5598
5993
|
} catch (err) {
|
|
5599
5994
|
setAuthStatus(msg("authImportFailedStatus", { message: err.message }), true);
|
|
5600
5995
|
alert(msg("authImportFailedAlert", { message: err.message }));
|
|
@@ -5603,6 +5998,45 @@ ${scenarioLab.length ? `
|
|
|
5603
5998
|
reader.readAsText(file, "utf-8");
|
|
5604
5999
|
}
|
|
5605
6000
|
|
|
6001
|
+
function openExportModeModal() {
|
|
6002
|
+
const modal = document.getElementById("exportModeModal");
|
|
6003
|
+
const chk = document.getElementById("chkExportPlainJson");
|
|
6004
|
+
if (!modal || !chk) {
|
|
6005
|
+
return Promise.resolve(null);
|
|
6006
|
+
}
|
|
6007
|
+
chk.checked = false;
|
|
6008
|
+
modal.classList.remove("is-hidden");
|
|
6009
|
+
modal.setAttribute("aria-hidden", "false");
|
|
6010
|
+
|
|
6011
|
+
return new Promise((resolve) => {
|
|
6012
|
+
const close = (confirmed) => {
|
|
6013
|
+
modal.classList.add("is-hidden");
|
|
6014
|
+
modal.setAttribute("aria-hidden", "true");
|
|
6015
|
+
confirmBtn.removeEventListener("click", onConfirm);
|
|
6016
|
+
cancelBtn.removeEventListener("click", onCancel);
|
|
6017
|
+
modal.removeEventListener("click", onBackdrop);
|
|
6018
|
+
resolve(confirmed ? chk.checked : null);
|
|
6019
|
+
};
|
|
6020
|
+
const confirmBtn = document.getElementById("btnConfirmExportMode");
|
|
6021
|
+
const cancelBtn = document.getElementById("btnCancelExportMode");
|
|
6022
|
+
const onConfirm = () => close(true);
|
|
6023
|
+
const onCancel = () => close(false);
|
|
6024
|
+
const onBackdrop = (evt) => {
|
|
6025
|
+
if (evt.target && evt.target.getAttribute("data-export-modal-close") === "1") {
|
|
6026
|
+
close(false);
|
|
6027
|
+
}
|
|
6028
|
+
};
|
|
6029
|
+
|
|
6030
|
+
if (!confirmBtn || !cancelBtn) {
|
|
6031
|
+
close(false);
|
|
6032
|
+
return;
|
|
6033
|
+
}
|
|
6034
|
+
confirmBtn.addEventListener("click", onConfirm);
|
|
6035
|
+
cancelBtn.addEventListener("click", onCancel);
|
|
6036
|
+
modal.addEventListener("click", onBackdrop);
|
|
6037
|
+
});
|
|
6038
|
+
}
|
|
6039
|
+
|
|
5606
6040
|
function serializeState() {
|
|
5607
6041
|
captureUiViewStateFromDom();
|
|
5608
6042
|
const calcProfileCfg = getCalcProfileConfig(getSelectedCalcProfileId());
|
|
@@ -5624,11 +6058,13 @@ ${scenarioLab.length ? `
|
|
|
5624
6058
|
assegnoFam2: num("assegnoFam2"),
|
|
5625
6059
|
primaCasaMutuoEnabled: document.getElementById("primaCasaMutuoEnabled")?.checked ? 1 : 0,
|
|
5626
6060
|
primaCasaMutuoImporto: num("primaCasaMutuoImporto"),
|
|
6061
|
+
primaCasaValoreLocativo: num("primaCasaValoreLocativo"),
|
|
5627
6062
|
primaCasaAssegnataA: String(document.getElementById("primaCasaAssegnataA")?.value || ""),
|
|
5628
6063
|
primaCasaMutuoPerc1: num("primaCasaMutuoPerc1"),
|
|
5629
6064
|
straordAnn1: num("straordAnn1"),
|
|
5630
6065
|
straordAnn2: num("straordAnn2"),
|
|
5631
|
-
speseConvivenza: num("speseConvivenza")
|
|
6066
|
+
speseConvivenza: num("speseConvivenza"),
|
|
6067
|
+
speseConvivenzaAuto: speseConvivenzaAutoMode ? 1 : 0
|
|
5632
6068
|
};
|
|
5633
6069
|
const spese = expenseItems.map((_, i) => ({
|
|
5634
6070
|
c1: num(`c1_${i}`),
|
|
@@ -5674,6 +6110,11 @@ ${scenarioLab.length ? `
|
|
|
5674
6110
|
el.value = v;
|
|
5675
6111
|
}
|
|
5676
6112
|
});
|
|
6113
|
+
if (state.base && Object.prototype.hasOwnProperty.call(state.base, "speseConvivenzaAuto")) {
|
|
6114
|
+
speseConvivenzaAutoMode = Number(state.base.speseConvivenzaAuto || 0) > 0;
|
|
6115
|
+
} else {
|
|
6116
|
+
speseConvivenzaAutoMode = Math.max(0, Number(state.base && state.base.speseConvivenza || 0)) <= 0.005;
|
|
6117
|
+
}
|
|
5677
6118
|
if (Array.isArray(state.expenseItems) && state.expenseItems.length) {
|
|
5678
6119
|
expenseItems = state.expenseItems.map((item, idx) => normalizeExpenseItem(item, idx));
|
|
5679
6120
|
} else {
|
|
@@ -5743,6 +6184,7 @@ ${scenarioLab.length ? `
|
|
|
5743
6184
|
if (firstHomeEnabled) firstHomeEnabled.checked = !!firstHomeEnabled.defaultChecked;
|
|
5744
6185
|
if (firstHomeAssigned) firstHomeAssigned.value = "";
|
|
5745
6186
|
permanenceCalendarState.byMonth = {};
|
|
6187
|
+
speseConvivenzaAutoMode = true;
|
|
5746
6188
|
selectedScenarioIdx = -1;
|
|
5747
6189
|
uiViewState.spiegOpen = true;
|
|
5748
6190
|
uiViewState.formulaOpen = false;
|
|
@@ -5764,7 +6206,11 @@ ${scenarioLab.length ? `
|
|
|
5764
6206
|
|
|
5765
6207
|
document.getElementById("btnExportJson").addEventListener("click", async () => {
|
|
5766
6208
|
if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnExportJson")); return; }
|
|
5767
|
-
await
|
|
6209
|
+
const plainExport = await openExportModeModal();
|
|
6210
|
+
if (plainExport === null) {
|
|
6211
|
+
return;
|
|
6212
|
+
}
|
|
6213
|
+
await exportJson({ plain: plainExport });
|
|
5768
6214
|
});
|
|
5769
6215
|
|
|
5770
6216
|
document.getElementById("btnImportJson").addEventListener("click", () => {
|
|
@@ -5896,6 +6342,13 @@ ${scenarioLab.length ? `
|
|
|
5896
6342
|
}
|
|
5897
6343
|
});
|
|
5898
6344
|
|
|
6345
|
+
const speseConvivenzaInput = document.getElementById("speseConvivenza");
|
|
6346
|
+
if (speseConvivenzaInput) {
|
|
6347
|
+
speseConvivenzaInput.addEventListener("input", () => {
|
|
6348
|
+
speseConvivenzaAutoMode = false;
|
|
6349
|
+
});
|
|
6350
|
+
}
|
|
6351
|
+
|
|
5899
6352
|
document.getElementById("btnZoomOut").addEventListener("click", () => {
|
|
5900
6353
|
const current = normalizeUiZoom(getComputedStyle(document.documentElement).getPropertyValue("--ui-zoom"));
|
|
5901
6354
|
setUiZoom(current - UI_ZOOM_STEP);
|
|
@@ -6003,7 +6456,7 @@ ${scenarioLab.length ? `
|
|
|
6003
6456
|
syncPermanenza("perm1");
|
|
6004
6457
|
} else if (e.target.id === "perm2") {
|
|
6005
6458
|
syncPermanenza("perm2");
|
|
6006
|
-
} else if (e.target.id === "primaCasaMutuoPerc1" || e.target.id === "primaCasaMutuoImporto") {
|
|
6459
|
+
} else if (e.target.id === "primaCasaMutuoPerc1" || e.target.id === "primaCasaMutuoImporto" || e.target.id === "primaCasaValoreLocativo") {
|
|
6007
6460
|
updateFirstHomeMortgageUi();
|
|
6008
6461
|
} else if (e.target.id === "reddito1" || e.target.id === "reddito2") {
|
|
6009
6462
|
const activeMode = document.getElementById("incomeMode")?.value || "monthly";
|
|
@@ -6069,6 +6522,9 @@ ${scenarioLab.length ? `
|
|
|
6069
6522
|
void initVisitorCounters();
|
|
6070
6523
|
setAuthMode("login");
|
|
6071
6524
|
updateAuthUi();
|
|
6525
|
+
void restorePersistedAuthSession().finally(() => {
|
|
6526
|
+
void maybeAutoLoginFromUrl();
|
|
6527
|
+
});
|
|
6072
6528
|
renderCloudHistoryPanel();
|
|
6073
6529
|
applyUiViewStateToDom();
|
|
6074
6530
|
updateFirstHomeMortgageUi();
|