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.
@@ -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 cifrato",
63
- btnImport: "Carica JSON cifrato",
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 KeyLock",
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 se esiste un mutuo sulla prima casa dei coniugi ceduta a uno dei due: il modello considera il trasferimento implicito quando la casa e assegnata al collocatario.",
268
- firstHomeMortgageEnabledLabel: "Mutuo su prima casa dei coniugi",
269
- firstHomeMortgageEnabledHint: "Attiva per includere il mutuo della prima casa ceduta nei benefici compensativi.",
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 encrypted JSON",
405
- btnImport: "Import encrypted JSON",
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: an encrypted KeyLock export is required",
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 whether there is a mortgage on the spouses' primary home assigned to one spouse: the model counts the implicit transfer when the home is assigned to the custodial parent.",
610
- firstHomeMortgageEnabledLabel: "Mortgage on spouses' primary home",
611
- firstHomeMortgageEnabledHint: "Enable to include the assigned primary-home mortgage in compensative benefits.",
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: false,
749
- autoRefreshToken: false,
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 = localStorage.getItem(`m_donor_${user.id}`) === "1";
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 primaCasaConsidered = primaCasaMutuoEnabled && primaCasaMutuoImporto > 0
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
- assegnoDa1a2 = Math.max(0, assegnoDa1a2 - primaCasaTransfer1to2);
3430
- assegnoDa2a1 = Math.max(0, assegnoDa2a1 - primaCasaTransfer2to1);
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">&#127873;&ensp;${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} &rarr; ${n2}</div>
4341
4676
  <div class="spieg-result-formula">${n1}: ${eur(m.quotaTeorica1)} &minus; ${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} &rarr; ${n1}</div>
4348
4684
  <div class="spieg-result-formula">${n2}: ${eur(m.quotaTeorica2)} &minus; ${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
- const benefitRows = getCompensativeBenefitRows(m, c1n(), c2n());
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">&#9878;&#65039;&ensp;${escapeHtml(tr("calcNoTransferSuggested"))}</div>
4369
- <div class="spieg-benefits-section">
4370
- <div class="spieg-benefits-label">&#127873;&ensp;${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">&#127873;&ensp;${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)}&thinsp;${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">&thinsp;${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)}&thinsp;${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">&thinsp;${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">&#127873;&ensp;${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">&#9878;&#65039;&ensp;${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">&#127873;&ensp;${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} &rarr; ${c2NameEsc}</div>
4823
5173
  <div class="pdf-explain-formula">${c1NameEsc}: ${eur(m.quotaTeorica1)} &minus; ${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} &rarr; ${c1NameEsc}</div>
4829
5180
  <div class="pdf-explain-formula">${c2NameEsc}: ${eur(m.quotaTeorica2)} &minus; ${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-result-empty">${tr("calcNoTransferSuggested")}</div>
4835
- <div class="pdf-explain-formula"><strong>${tr("calcCompBenefitsLabel")}:</strong> ${compBenefits.map((row) => `${escapeHtml(row.label)} (${eur(row.amount)})`).join(" | ")}</div>
5186
+ <div class="pdf-explain-no-transfer-badge">&#9878;&#65039;&ensp;${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 separationSectionHtml = m.speseConvivenza > 0
5006
- ? `
5007
- <div class="section">
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
- const encrypted = await encryptStateForKey(state, authSession.keyBits);
5554
- const payloadObj = {
5555
- format: "keylock-encrypted-state-v1",
5556
- owner: authSession.username,
5557
- createdAt: new Date().toISOString(),
5558
- cipher: encrypted
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 = `mantenimento-cifrato-${authSession.username}-${ts}.json`;
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 !== "keylock-encrypted-state-v1" || !payload.owner || !payload.cipher) {
5955
+ if (!payload || !payload.format || !payload.owner) {
5579
5956
  throw new Error(tr("authInvalidJsonFormat"));
5580
5957
  }
5581
5958
 
5582
- if (!authSession.username || !authSession.keyBits) {
5959
+ if (!authSession.username) {
5583
5960
  throw new Error(tr("authLoginBeforeImport"));
5584
5961
  }
5585
5962
 
5586
- if (normalizeUsername(payload.owner) !== normalizeUsername(authSession.username)) {
5587
- throw new Error(tr("authFileOwnedByOther"));
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
- setAuthStatus(tr("authEncryptedJsonImported"));
5597
- alert(tr("authEncryptedJsonLoaded"));
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 exportJson();
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();