mantenimento-app 2.4.1 → 2.4.3

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/app.js CHANGED
@@ -281,6 +281,8 @@ const defaultExpenseItems = [
281
281
  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.",
282
282
  firstHomeMortgageEnabledLabel: "Mutuo su prima casa dei coniugi",
283
283
  firstHomeMortgageEnabledHint: "Attiva per includere il mutuo della prima casa ceduta nei benefici compensativi.",
284
+ firstHomeLocativeValueLabel: "Casa (valore locativo) ({currency})",
285
+ firstHomeLocativeValueHint: "Valore locativo mensile della casa assegnata, usato per valorizzare il beneficio economico implicito.",
284
286
  firstHomeMortgageAmountLabel: "Rata mutuo mensile ({currency})",
285
287
  firstHomeMortgageAmountHint: "Importo mensile complessivo della rata del mutuo prima casa.",
286
288
  firstHomeAssignedToLabel: "Casa assegnata a",
@@ -295,6 +297,7 @@ const defaultExpenseItems = [
295
297
  calcBenefitFamilyAllowance: "Assegno familiare INPS percepito da {spouse}",
296
298
  calcBenefitPrimaryHomeMortgage: "Quota mutuo prima casa ceduta al collocatario ({payer} -> {receiver})",
297
299
  calcBenefitPrimaryHomeAssignment: "Assegnazione casa familiare - valore locativo ({receiver})",
300
+ calcBenefitExcludedFromTotal: "fuori totale",
298
301
  pdfCompBenefitsSection: "Benefici compensativi gia allocati",
299
302
  pdfCompBenefitsItem: "Beneficio",
300
303
  pdfCompBenefitsAmount: "Valore {currency}/mese",
@@ -640,6 +643,8 @@ const defaultExpenseItems = [
640
643
  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.",
641
644
  firstHomeMortgageEnabledLabel: "Mortgage on spouses' primary home",
642
645
  firstHomeMortgageEnabledHint: "Enable to include the assigned primary-home mortgage in compensative benefits.",
646
+ firstHomeLocativeValueLabel: "Home (rental value) ({currency})",
647
+ firstHomeLocativeValueHint: "Monthly rental value of the assigned home, used to value the implicit economic benefit.",
643
648
  firstHomeMortgageAmountLabel: "Monthly mortgage payment ({currency})",
644
649
  firstHomeMortgageAmountHint: "Total monthly amount of the primary-home mortgage payment.",
645
650
  firstHomeAssignedToLabel: "Home assigned to",
@@ -654,6 +659,7 @@ const defaultExpenseItems = [
654
659
  calcBenefitFamilyAllowance: "INPS family allowance received by {spouse}",
655
660
  calcBenefitPrimaryHomeMortgage: "Primary-home mortgage share assigned to custodial parent ({payer} -> {receiver})",
656
661
  calcBenefitPrimaryHomeAssignment: "Primary home assignment - rental value ({receiver})",
662
+ calcBenefitExcludedFromTotal: "excluded from total",
657
663
  pdfCompBenefitsSection: "Compensative benefits already allocated",
658
664
  pdfCompBenefitsItem: "Benefit",
659
665
  pdfCompBenefitsAmount: "Value {currency}/month",
@@ -1213,6 +1219,8 @@ const defaultExpenseItems = [
1213
1219
  const firstHomeBoxNote = document.getElementById("firstHomeBoxNote");
1214
1220
  const lblPrimaCasaMutuoEnabled = document.getElementById("lblPrimaCasaMutuoEnabled");
1215
1221
  const hintPrimaCasaMutuoEnabled = document.getElementById("hintPrimaCasaMutuoEnabled");
1222
+ const lblPrimaCasaValoreLocativo = document.getElementById("lblPrimaCasaValoreLocativo");
1223
+ const hintPrimaCasaValoreLocativo = document.getElementById("hintPrimaCasaValoreLocativo");
1216
1224
  const lblPrimaCasaMutuoImporto = document.getElementById("lblPrimaCasaMutuoImporto");
1217
1225
  const hintPrimaCasaMutuoImporto = document.getElementById("hintPrimaCasaMutuoImporto");
1218
1226
  const lblPrimaCasaAssegnataA = document.getElementById("lblPrimaCasaAssegnataA");
@@ -1276,6 +1284,8 @@ const defaultExpenseItems = [
1276
1284
  if (firstHomeBoxNote) firstHomeBoxNote.textContent = tr("firstHomeBoxNote");
1277
1285
  if (lblPrimaCasaMutuoEnabled) lblPrimaCasaMutuoEnabled.textContent = tr("firstHomeMortgageEnabledLabel");
1278
1286
  if (hintPrimaCasaMutuoEnabled) hintPrimaCasaMutuoEnabled.title = tr("firstHomeMortgageEnabledHint");
1287
+ if (lblPrimaCasaValoreLocativo) lblPrimaCasaValoreLocativo.textContent = msg("firstHomeLocativeValueLabel", { currency: currentCurrency });
1288
+ if (hintPrimaCasaValoreLocativo) hintPrimaCasaValoreLocativo.title = tr("firstHomeLocativeValueHint");
1279
1289
  if (lblPrimaCasaMutuoImporto) lblPrimaCasaMutuoImporto.textContent = msg("firstHomeMortgageAmountLabel", { currency: currentCurrency });
1280
1290
  if (hintPrimaCasaMutuoImporto) hintPrimaCasaMutuoImporto.title = tr("firstHomeMortgageAmountHint");
1281
1291
  if (lblPrimaCasaAssegnataA) lblPrimaCasaAssegnataA.textContent = tr("firstHomeAssignedToLabel");
@@ -1496,6 +1506,14 @@ const defaultExpenseItems = [
1496
1506
  return { label, help };
1497
1507
  }
1498
1508
 
1509
+ function isLegacyLocativeExpenseLabel(label) {
1510
+ const normalized = String(label || "")
1511
+ .replace(/^[^A-Za-z\u00C0-\u024F]+/, "")
1512
+ .trim()
1513
+ .toLowerCase();
1514
+ return normalized === "casa (valore locativo)";
1515
+ }
1516
+
1499
1517
  function populateSuggestedExpenseOptions() {
1500
1518
  const select = document.getElementById("suggestedExpenseSelect");
1501
1519
  if (!select) return;
@@ -3589,6 +3607,7 @@ const defaultExpenseItems = [
3589
3607
  aFam1: num("assegnoFam1"),
3590
3608
  aFam2: num("assegnoFam2"),
3591
3609
  primaCasaMutuoEnabled: firstHome.enabled ? 1 : 0,
3610
+ primaCasaValoreLocativo: num("primaCasaValoreLocativo"),
3592
3611
  primaCasaMutuoImporto: firstHome.amount,
3593
3612
  primaCasaAssegnataA: firstHome.assignedTo,
3594
3613
  primaCasaMutuoPerc1: firstHome.share1,
@@ -4638,14 +4657,14 @@ const defaultExpenseItems = [
4638
4657
  const rawBenefs = Array.isArray(m.compensativeBenefits)
4639
4658
  ? m.compensativeBenefits.filter((r) => r && Number(r.amount || 0) > 0.005)
4640
4659
  : [];
4641
- const typeIcons = { family: "\uD83C\uDFDB", "primary-home-mortgage": "\uD83C\uDFE1" };
4660
+ const typeIcons = { family: "\uD83C\uDFDB", "primary-home-mortgage": "\uD83C\uDFE1", "primary-home-assignment": "\uD83C\uDFE0" };
4642
4661
  const cardsHtml = benefitRows
4643
4662
  .map((row, i) => {
4644
4663
  const icon = (rawBenefs[i] && typeIcons[rawBenefs[i].type]) || "\u2726";
4645
4664
  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>`;
4646
4665
  })
4647
4666
  .join("");
4648
- const total = benefitRows.reduce((s, r) => s + r.amount, 0);
4667
+ const total = getCompensativeBenefitsTotal(benefitRows);
4649
4668
  const totalLabel = currentLang === "en" ? "Total allocated benefits" : "Totale benefici allocati";
4650
4669
  resultHtml = `
4651
4670
  <div class="spieg-no-transfer-badge">&#9878;&#65039;&ensp;${escapeHtml(tr("calcNoTransferSuggested"))}</div>
@@ -4730,21 +4749,32 @@ const defaultExpenseItems = [
4730
4749
  const amount = Number(row.amount || 0);
4731
4750
  if (row.type === "family") {
4732
4751
  const spouse = Number(row.to) === 2 ? name2 : name1;
4733
- return { label: msg("calcBenefitFamilyAllowance", { spouse }), amount };
4752
+ return { label: msg("calcBenefitFamilyAllowance", { spouse }), amount, includeInTotal: true };
4734
4753
  }
4735
4754
  if (row.type === "primary-home-mortgage") {
4736
4755
  const payer = Number(row.from) === 2 ? name2 : name1;
4737
4756
  const receiver = Number(row.to) === 2 ? name2 : name1;
4738
- return { label: msg("calcBenefitPrimaryHomeMortgage", { payer, receiver }), amount };
4757
+ return { label: msg("calcBenefitPrimaryHomeMortgage", { payer, receiver }), amount, includeInTotal: true };
4739
4758
  }
4740
4759
  if (row.type === "primary-home-assignment") {
4741
4760
  const receiver = Number(row.to) === 2 ? name2 : name1;
4742
- return { label: msg("calcBenefitPrimaryHomeAssignment", { receiver }), amount };
4761
+ return {
4762
+ label: `${msg("calcBenefitPrimaryHomeAssignment", { receiver })} (${tr("calcBenefitExcludedFromTotal")})`,
4763
+ amount,
4764
+ includeInTotal: false
4765
+ };
4743
4766
  }
4744
- return { label: tr("calcCompBenefitsLabel"), amount };
4767
+ return { label: tr("calcCompBenefitsLabel"), amount, includeInTotal: true };
4745
4768
  });
4746
4769
  }
4747
4770
 
4771
+ function getCompensativeBenefitsTotal(rows) {
4772
+ return (rows || []).reduce((sum, row) => {
4773
+ if (!row || row.includeInTotal === false) return sum;
4774
+ return sum + Number(row.amount || 0);
4775
+ }, 0);
4776
+ }
4777
+
4748
4778
  function formatCompensativeBenefitsInline(m, name1 = c1n(), name2 = c2n()) {
4749
4779
  return getCompensativeBenefitRows(m, name1, name2)
4750
4780
  .map((row) => `${row.label}: ${eur(row.amount)}`)
@@ -4851,14 +4881,14 @@ const defaultExpenseItems = [
4851
4881
  const rawBenefs = Array.isArray(m.compensativeBenefits)
4852
4882
  ? m.compensativeBenefits.filter((r) => r && Number(r.amount || 0) > 0.005)
4853
4883
  : [];
4854
- const typeIcons = { family: "\uD83C\uDFDB", "primary-home-mortgage": "\uD83C\uDFE1" };
4884
+ const typeIcons = { family: "\uD83C\uDFDB", "primary-home-mortgage": "\uD83C\uDFE1", "primary-home-assignment": "\uD83C\uDFE0" };
4855
4885
  const cardsHtml = benefitRows
4856
4886
  .map((row, i) => {
4857
4887
  const icon = (rawBenefs[i] && typeIcons[rawBenefs[i].type]) || "\u2726";
4858
4888
  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>`;
4859
4889
  })
4860
4890
  .join("");
4861
- const total = benefitRows.reduce((s, r) => s + r.amount, 0);
4891
+ const total = getCompensativeBenefitsTotal(benefitRows);
4862
4892
  const totalLabel = currentLang === "en" ? "Total allocated benefits" : "Totale benefici allocati";
4863
4893
  benefitCardsHtml = `
4864
4894
  <div class="result-benefits-box">
@@ -5122,12 +5152,12 @@ const defaultExpenseItems = [
5122
5152
  const rawBenefs = Array.isArray(m.compensativeBenefits)
5123
5153
  ? m.compensativeBenefits.filter((row) => row && Number(row.amount || 0) > 0.005)
5124
5154
  : [];
5125
- const typeIcons = { family: "\uD83C\uDFDB", "primary-home-mortgage": "\uD83C\uDFE1" };
5155
+ const typeIcons = { family: "\uD83C\uDFDB", "primary-home-mortgage": "\uD83C\uDFE1", "primary-home-assignment": "\uD83C\uDFE0" };
5126
5156
  const cardsHtml = compBenefits.map((row, idx) => {
5127
5157
  const icon = (rawBenefs[idx] && typeIcons[rawBenefs[idx].type]) || "\u2726";
5128
5158
  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>`;
5129
5159
  }).join("");
5130
- const benefitsTotal = compBenefits.reduce((sum, row) => sum + Number(row.amount || 0), 0);
5160
+ const benefitsTotal = getCompensativeBenefitsTotal(compBenefits);
5131
5161
  explainResultHtml = `
5132
5162
  <div class="pdf-explain-no-transfer-badge">&#9878;&#65039;&ensp;${escapeHtml(tr("calcNoTransferSuggested"))}</div>
5133
5163
  <div class="pdf-explain-benefits-section">
@@ -6007,6 +6037,7 @@ ${scenarioLab.length ? `
6007
6037
  assegnoPagato2: num("assegnoPagato2"),
6008
6038
  assegnoFam2: num("assegnoFam2"),
6009
6039
  primaCasaMutuoEnabled: document.getElementById("primaCasaMutuoEnabled")?.checked ? 1 : 0,
6040
+ primaCasaValoreLocativo: num("primaCasaValoreLocativo"),
6010
6041
  primaCasaMutuoImporto: num("primaCasaMutuoImporto"),
6011
6042
  primaCasaAssegnataA: String(document.getElementById("primaCasaAssegnataA")?.value || ""),
6012
6043
  primaCasaMutuoPerc1: num("primaCasaMutuoPerc1"),
@@ -6050,6 +6081,7 @@ ${scenarioLab.length ? `
6050
6081
 
6051
6082
  function hydrateState(state) {
6052
6083
  if (!state || !state.base || !state.spese) return;
6084
+ let stateSpeseRows = Array.isArray(state.spese) ? state.spese : [];
6053
6085
  Object.entries(state.base).forEach(([k, v]) => {
6054
6086
  const el = document.getElementById(k);
6055
6087
  if (!el) return;
@@ -6065,11 +6097,15 @@ ${scenarioLab.length ? `
6065
6097
  speseConvivenzaAutoMode = Math.max(0, Number(state.base && state.base.speseConvivenza || 0)) <= 0.005;
6066
6098
  }
6067
6099
  if (Array.isArray(state.expenseItems) && state.expenseItems.length) {
6068
- expenseItems = state.expenseItems.map((item, idx) => normalizeExpenseItem(item, idx));
6100
+ const filteredPairs = state.expenseItems
6101
+ .map((item, idx) => ({ item, row: stateSpeseRows[idx] }))
6102
+ .filter((pair) => !isLegacyLocativeExpenseLabel(pair && pair.item && pair.item.label));
6103
+ expenseItems = filteredPairs.map((pair, idx) => normalizeExpenseItem(pair.item, idx));
6104
+ stateSpeseRows = filteredPairs.map((pair) => pair.row).filter((row) => row !== undefined);
6069
6105
  } else {
6070
6106
  expenseItems = defaultExpenseItems.map((item) => ({ ...item }));
6071
6107
  }
6072
- while (expenseItems.length < state.spese.length) {
6108
+ while (expenseItems.length < stateSpeseRows.length) {
6073
6109
  expenseItems.push(normalizeExpenseItem(null, expenseItems.length));
6074
6110
  }
6075
6111
  scenarioLab = normalizeScenarioLabState(state.scenarioLab);
@@ -6107,7 +6143,7 @@ ${scenarioLab.length ? `
6107
6143
  updateSpouseLabels();
6108
6144
  buildExpenseRows();
6109
6145
  syncPermanenza("calendar");
6110
- state.spese.forEach((row, i) => {
6146
+ stateSpeseRows.forEach((row, i) => {
6111
6147
  const c1 = document.getElementById(`c1_${i}`);
6112
6148
  const c2 = document.getElementById(`c2_${i}`);
6113
6149
  const d1 = document.getElementById(`c1d_${i}`);
@@ -281,6 +281,8 @@ const defaultExpenseItems = [
281
281
  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.",
282
282
  firstHomeMortgageEnabledLabel: "Mutuo su prima casa dei coniugi",
283
283
  firstHomeMortgageEnabledHint: "Attiva per includere il mutuo della prima casa ceduta nei benefici compensativi.",
284
+ firstHomeLocativeValueLabel: "Casa (valore locativo) ({currency})",
285
+ firstHomeLocativeValueHint: "Valore locativo mensile della casa assegnata, usato per valorizzare il beneficio economico implicito.",
284
286
  firstHomeMortgageAmountLabel: "Rata mutuo mensile ({currency})",
285
287
  firstHomeMortgageAmountHint: "Importo mensile complessivo della rata del mutuo prima casa.",
286
288
  firstHomeAssignedToLabel: "Casa assegnata a",
@@ -295,6 +297,7 @@ const defaultExpenseItems = [
295
297
  calcBenefitFamilyAllowance: "Assegno familiare INPS percepito da {spouse}",
296
298
  calcBenefitPrimaryHomeMortgage: "Quota mutuo prima casa ceduta al collocatario ({payer} -> {receiver})",
297
299
  calcBenefitPrimaryHomeAssignment: "Assegnazione casa familiare - valore locativo ({receiver})",
300
+ calcBenefitExcludedFromTotal: "fuori totale",
298
301
  pdfCompBenefitsSection: "Benefici compensativi gia allocati",
299
302
  pdfCompBenefitsItem: "Beneficio",
300
303
  pdfCompBenefitsAmount: "Valore {currency}/mese",
@@ -640,6 +643,8 @@ const defaultExpenseItems = [
640
643
  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.",
641
644
  firstHomeMortgageEnabledLabel: "Mortgage on spouses' primary home",
642
645
  firstHomeMortgageEnabledHint: "Enable to include the assigned primary-home mortgage in compensative benefits.",
646
+ firstHomeLocativeValueLabel: "Home (rental value) ({currency})",
647
+ firstHomeLocativeValueHint: "Monthly rental value of the assigned home, used to value the implicit economic benefit.",
643
648
  firstHomeMortgageAmountLabel: "Monthly mortgage payment ({currency})",
644
649
  firstHomeMortgageAmountHint: "Total monthly amount of the primary-home mortgage payment.",
645
650
  firstHomeAssignedToLabel: "Home assigned to",
@@ -654,6 +659,7 @@ const defaultExpenseItems = [
654
659
  calcBenefitFamilyAllowance: "INPS family allowance received by {spouse}",
655
660
  calcBenefitPrimaryHomeMortgage: "Primary-home mortgage share assigned to custodial parent ({payer} -> {receiver})",
656
661
  calcBenefitPrimaryHomeAssignment: "Primary home assignment - rental value ({receiver})",
662
+ calcBenefitExcludedFromTotal: "excluded from total",
657
663
  pdfCompBenefitsSection: "Compensative benefits already allocated",
658
664
  pdfCompBenefitsItem: "Benefit",
659
665
  pdfCompBenefitsAmount: "Value {currency}/month",
@@ -1213,6 +1219,8 @@ const defaultExpenseItems = [
1213
1219
  const firstHomeBoxNote = document.getElementById("firstHomeBoxNote");
1214
1220
  const lblPrimaCasaMutuoEnabled = document.getElementById("lblPrimaCasaMutuoEnabled");
1215
1221
  const hintPrimaCasaMutuoEnabled = document.getElementById("hintPrimaCasaMutuoEnabled");
1222
+ const lblPrimaCasaValoreLocativo = document.getElementById("lblPrimaCasaValoreLocativo");
1223
+ const hintPrimaCasaValoreLocativo = document.getElementById("hintPrimaCasaValoreLocativo");
1216
1224
  const lblPrimaCasaMutuoImporto = document.getElementById("lblPrimaCasaMutuoImporto");
1217
1225
  const hintPrimaCasaMutuoImporto = document.getElementById("hintPrimaCasaMutuoImporto");
1218
1226
  const lblPrimaCasaAssegnataA = document.getElementById("lblPrimaCasaAssegnataA");
@@ -1276,6 +1284,8 @@ const defaultExpenseItems = [
1276
1284
  if (firstHomeBoxNote) firstHomeBoxNote.textContent = tr("firstHomeBoxNote");
1277
1285
  if (lblPrimaCasaMutuoEnabled) lblPrimaCasaMutuoEnabled.textContent = tr("firstHomeMortgageEnabledLabel");
1278
1286
  if (hintPrimaCasaMutuoEnabled) hintPrimaCasaMutuoEnabled.title = tr("firstHomeMortgageEnabledHint");
1287
+ if (lblPrimaCasaValoreLocativo) lblPrimaCasaValoreLocativo.textContent = msg("firstHomeLocativeValueLabel", { currency: currentCurrency });
1288
+ if (hintPrimaCasaValoreLocativo) hintPrimaCasaValoreLocativo.title = tr("firstHomeLocativeValueHint");
1279
1289
  if (lblPrimaCasaMutuoImporto) lblPrimaCasaMutuoImporto.textContent = msg("firstHomeMortgageAmountLabel", { currency: currentCurrency });
1280
1290
  if (hintPrimaCasaMutuoImporto) hintPrimaCasaMutuoImporto.title = tr("firstHomeMortgageAmountHint");
1281
1291
  if (lblPrimaCasaAssegnataA) lblPrimaCasaAssegnataA.textContent = tr("firstHomeAssignedToLabel");
@@ -1496,6 +1506,14 @@ const defaultExpenseItems = [
1496
1506
  return { label, help };
1497
1507
  }
1498
1508
 
1509
+ function isLegacyLocativeExpenseLabel(label) {
1510
+ const normalized = String(label || "")
1511
+ .replace(/^[^A-Za-z\u00C0-\u024F]+/, "")
1512
+ .trim()
1513
+ .toLowerCase();
1514
+ return normalized === "casa (valore locativo)";
1515
+ }
1516
+
1499
1517
  function populateSuggestedExpenseOptions() {
1500
1518
  const select = document.getElementById("suggestedExpenseSelect");
1501
1519
  if (!select) return;
@@ -3589,6 +3607,7 @@ const defaultExpenseItems = [
3589
3607
  aFam1: num("assegnoFam1"),
3590
3608
  aFam2: num("assegnoFam2"),
3591
3609
  primaCasaMutuoEnabled: firstHome.enabled ? 1 : 0,
3610
+ primaCasaValoreLocativo: num("primaCasaValoreLocativo"),
3592
3611
  primaCasaMutuoImporto: firstHome.amount,
3593
3612
  primaCasaAssegnataA: firstHome.assignedTo,
3594
3613
  primaCasaMutuoPerc1: firstHome.share1,
@@ -4638,14 +4657,14 @@ const defaultExpenseItems = [
4638
4657
  const rawBenefs = Array.isArray(m.compensativeBenefits)
4639
4658
  ? m.compensativeBenefits.filter((r) => r && Number(r.amount || 0) > 0.005)
4640
4659
  : [];
4641
- const typeIcons = { family: "\uD83C\uDFDB", "primary-home-mortgage": "\uD83C\uDFE1" };
4660
+ const typeIcons = { family: "\uD83C\uDFDB", "primary-home-mortgage": "\uD83C\uDFE1", "primary-home-assignment": "\uD83C\uDFE0" };
4642
4661
  const cardsHtml = benefitRows
4643
4662
  .map((row, i) => {
4644
4663
  const icon = (rawBenefs[i] && typeIcons[rawBenefs[i].type]) || "\u2726";
4645
4664
  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>`;
4646
4665
  })
4647
4666
  .join("");
4648
- const total = benefitRows.reduce((s, r) => s + r.amount, 0);
4667
+ const total = getCompensativeBenefitsTotal(benefitRows);
4649
4668
  const totalLabel = currentLang === "en" ? "Total allocated benefits" : "Totale benefici allocati";
4650
4669
  resultHtml = `
4651
4670
  <div class="spieg-no-transfer-badge">&#9878;&#65039;&ensp;${escapeHtml(tr("calcNoTransferSuggested"))}</div>
@@ -4730,21 +4749,32 @@ const defaultExpenseItems = [
4730
4749
  const amount = Number(row.amount || 0);
4731
4750
  if (row.type === "family") {
4732
4751
  const spouse = Number(row.to) === 2 ? name2 : name1;
4733
- return { label: msg("calcBenefitFamilyAllowance", { spouse }), amount };
4752
+ return { label: msg("calcBenefitFamilyAllowance", { spouse }), amount, includeInTotal: true };
4734
4753
  }
4735
4754
  if (row.type === "primary-home-mortgage") {
4736
4755
  const payer = Number(row.from) === 2 ? name2 : name1;
4737
4756
  const receiver = Number(row.to) === 2 ? name2 : name1;
4738
- return { label: msg("calcBenefitPrimaryHomeMortgage", { payer, receiver }), amount };
4757
+ return { label: msg("calcBenefitPrimaryHomeMortgage", { payer, receiver }), amount, includeInTotal: true };
4739
4758
  }
4740
4759
  if (row.type === "primary-home-assignment") {
4741
4760
  const receiver = Number(row.to) === 2 ? name2 : name1;
4742
- return { label: msg("calcBenefitPrimaryHomeAssignment", { receiver }), amount };
4761
+ return {
4762
+ label: `${msg("calcBenefitPrimaryHomeAssignment", { receiver })} (${tr("calcBenefitExcludedFromTotal")})`,
4763
+ amount,
4764
+ includeInTotal: false
4765
+ };
4743
4766
  }
4744
- return { label: tr("calcCompBenefitsLabel"), amount };
4767
+ return { label: tr("calcCompBenefitsLabel"), amount, includeInTotal: true };
4745
4768
  });
4746
4769
  }
4747
4770
 
4771
+ function getCompensativeBenefitsTotal(rows) {
4772
+ return (rows || []).reduce((sum, row) => {
4773
+ if (!row || row.includeInTotal === false) return sum;
4774
+ return sum + Number(row.amount || 0);
4775
+ }, 0);
4776
+ }
4777
+
4748
4778
  function formatCompensativeBenefitsInline(m, name1 = c1n(), name2 = c2n()) {
4749
4779
  return getCompensativeBenefitRows(m, name1, name2)
4750
4780
  .map((row) => `${row.label}: ${eur(row.amount)}`)
@@ -4851,14 +4881,14 @@ const defaultExpenseItems = [
4851
4881
  const rawBenefs = Array.isArray(m.compensativeBenefits)
4852
4882
  ? m.compensativeBenefits.filter((r) => r && Number(r.amount || 0) > 0.005)
4853
4883
  : [];
4854
- const typeIcons = { family: "\uD83C\uDFDB", "primary-home-mortgage": "\uD83C\uDFE1" };
4884
+ const typeIcons = { family: "\uD83C\uDFDB", "primary-home-mortgage": "\uD83C\uDFE1", "primary-home-assignment": "\uD83C\uDFE0" };
4855
4885
  const cardsHtml = benefitRows
4856
4886
  .map((row, i) => {
4857
4887
  const icon = (rawBenefs[i] && typeIcons[rawBenefs[i].type]) || "\u2726";
4858
4888
  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>`;
4859
4889
  })
4860
4890
  .join("");
4861
- const total = benefitRows.reduce((s, r) => s + r.amount, 0);
4891
+ const total = getCompensativeBenefitsTotal(benefitRows);
4862
4892
  const totalLabel = currentLang === "en" ? "Total allocated benefits" : "Totale benefici allocati";
4863
4893
  benefitCardsHtml = `
4864
4894
  <div class="result-benefits-box">
@@ -5122,12 +5152,12 @@ const defaultExpenseItems = [
5122
5152
  const rawBenefs = Array.isArray(m.compensativeBenefits)
5123
5153
  ? m.compensativeBenefits.filter((row) => row && Number(row.amount || 0) > 0.005)
5124
5154
  : [];
5125
- const typeIcons = { family: "\uD83C\uDFDB", "primary-home-mortgage": "\uD83C\uDFE1" };
5155
+ const typeIcons = { family: "\uD83C\uDFDB", "primary-home-mortgage": "\uD83C\uDFE1", "primary-home-assignment": "\uD83C\uDFE0" };
5126
5156
  const cardsHtml = compBenefits.map((row, idx) => {
5127
5157
  const icon = (rawBenefs[idx] && typeIcons[rawBenefs[idx].type]) || "\u2726";
5128
5158
  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>`;
5129
5159
  }).join("");
5130
- const benefitsTotal = compBenefits.reduce((sum, row) => sum + Number(row.amount || 0), 0);
5160
+ const benefitsTotal = getCompensativeBenefitsTotal(compBenefits);
5131
5161
  explainResultHtml = `
5132
5162
  <div class="pdf-explain-no-transfer-badge">&#9878;&#65039;&ensp;${escapeHtml(tr("calcNoTransferSuggested"))}</div>
5133
5163
  <div class="pdf-explain-benefits-section">
@@ -6007,6 +6037,7 @@ ${scenarioLab.length ? `
6007
6037
  assegnoPagato2: num("assegnoPagato2"),
6008
6038
  assegnoFam2: num("assegnoFam2"),
6009
6039
  primaCasaMutuoEnabled: document.getElementById("primaCasaMutuoEnabled")?.checked ? 1 : 0,
6040
+ primaCasaValoreLocativo: num("primaCasaValoreLocativo"),
6010
6041
  primaCasaMutuoImporto: num("primaCasaMutuoImporto"),
6011
6042
  primaCasaAssegnataA: String(document.getElementById("primaCasaAssegnataA")?.value || ""),
6012
6043
  primaCasaMutuoPerc1: num("primaCasaMutuoPerc1"),
@@ -6050,6 +6081,7 @@ ${scenarioLab.length ? `
6050
6081
 
6051
6082
  function hydrateState(state) {
6052
6083
  if (!state || !state.base || !state.spese) return;
6084
+ let stateSpeseRows = Array.isArray(state.spese) ? state.spese : [];
6053
6085
  Object.entries(state.base).forEach(([k, v]) => {
6054
6086
  const el = document.getElementById(k);
6055
6087
  if (!el) return;
@@ -6065,11 +6097,15 @@ ${scenarioLab.length ? `
6065
6097
  speseConvivenzaAutoMode = Math.max(0, Number(state.base && state.base.speseConvivenza || 0)) <= 0.005;
6066
6098
  }
6067
6099
  if (Array.isArray(state.expenseItems) && state.expenseItems.length) {
6068
- expenseItems = state.expenseItems.map((item, idx) => normalizeExpenseItem(item, idx));
6100
+ const filteredPairs = state.expenseItems
6101
+ .map((item, idx) => ({ item, row: stateSpeseRows[idx] }))
6102
+ .filter((pair) => !isLegacyLocativeExpenseLabel(pair && pair.item && pair.item.label));
6103
+ expenseItems = filteredPairs.map((pair, idx) => normalizeExpenseItem(pair.item, idx));
6104
+ stateSpeseRows = filteredPairs.map((pair) => pair.row).filter((row) => row !== undefined);
6069
6105
  } else {
6070
6106
  expenseItems = defaultExpenseItems.map((item) => ({ ...item }));
6071
6107
  }
6072
- while (expenseItems.length < state.spese.length) {
6108
+ while (expenseItems.length < stateSpeseRows.length) {
6073
6109
  expenseItems.push(normalizeExpenseItem(null, expenseItems.length));
6074
6110
  }
6075
6111
  scenarioLab = normalizeScenarioLabState(state.scenarioLab);
@@ -6107,7 +6143,7 @@ ${scenarioLab.length ? `
6107
6143
  updateSpouseLabels();
6108
6144
  buildExpenseRows();
6109
6145
  syncPermanenza("calendar");
6110
- state.spese.forEach((row, i) => {
6146
+ stateSpeseRows.forEach((row, i) => {
6111
6147
  const c1 = document.getElementById(`c1_${i}`);
6112
6148
  const c2 = document.getElementById(`c2_${i}`);
6113
6149
  const d1 = document.getElementById(`c1d_${i}`);
@@ -626,7 +626,7 @@
626
626
  <script src="supabase.min.js"></script>
627
627
  <script src="fabric.min.js"></script>
628
628
  <script src="html2pdf.bundle.min.js"></script>
629
- <script src="app.js?v=2.4.1"></script>
629
+ <script src="app.js?v=2.4.3"></script>
630
630
  </body>
631
631
  </html>
632
632
 
@@ -885,21 +885,60 @@
885
885
  }
886
886
 
887
887
  .extra-box-first-home {
888
+ position: relative;
889
+ overflow: hidden;
888
890
  background: linear-gradient(135deg, #ffffff 0%, #f0fffe 50%, #f5faff 100%);
889
891
  border: 1px solid rgba(27, 141, 127, 0.15);
890
892
  box-shadow: 0 2px 8px rgba(27, 141, 127, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.8);
891
893
  }
892
894
 
895
+ .extra-box-first-home::after {
896
+ content: "";
897
+ position: absolute;
898
+ inset: -40% -20% auto;
899
+ height: 180px;
900
+ background: radial-gradient(circle at 50% 50%, rgba(61, 181, 160, 0.16), rgba(216, 154, 53, 0.08) 45%, transparent 70%);
901
+ pointer-events: none;
902
+ animation: first-home-aurora 7s ease-in-out infinite alternate;
903
+ }
904
+
893
905
  .extra-box-first-home .extra-box-title::before {
894
906
  content: '🏠 ';
895
907
  margin-right: 4px;
896
908
  }
897
909
 
898
910
  .extra-box-first-home .extra-grid {
911
+ position: relative;
912
+ z-index: 1;
899
913
  gap: 8px 10px;
900
914
  }
901
915
 
916
+ .extra-box-first-home .field {
917
+ transition: transform 0.2s ease, filter 0.2s ease;
918
+ }
919
+
920
+ .extra-box-first-home .field:focus-within {
921
+ transform: translateY(-1px);
922
+ filter: saturate(1.04);
923
+ }
924
+
925
+ .extra-box-first-home .field input[type="number"],
926
+ .extra-box-first-home .field select {
927
+ border-width: 1.5px;
928
+ box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8) inset;
929
+ transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
930
+ }
931
+
932
+ .extra-box-first-home .field input[type="number"]:focus,
933
+ .extra-box-first-home .field select:focus {
934
+ border-color: rgba(27, 141, 127, 0.4);
935
+ box-shadow: 0 0 0 3px rgba(27, 141, 127, 0.14), 0 1px 0 rgba(255, 255, 255, 0.85) inset;
936
+ background: linear-gradient(180deg, #ffffff, #f4fffd);
937
+ }
938
+
902
939
  .mortgage-split-slider {
940
+ position: relative;
941
+ overflow: hidden;
903
942
  margin-top: 12px;
904
943
  border: 1.5px solid rgba(27, 141, 127, 0.2);
905
944
  border-radius: 16px;
@@ -916,11 +955,25 @@
916
955
  transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
917
956
  }
918
957
 
958
+ .mortgage-split-slider::after {
959
+ content: "";
960
+ position: absolute;
961
+ inset: 0;
962
+ background: linear-gradient(112deg, transparent 30%, rgba(255, 255, 255, 0.55) 46%, transparent 58%);
963
+ transform: translateX(-120%);
964
+ transition: transform 0.5s ease;
965
+ pointer-events: none;
966
+ }
967
+
919
968
  .mortgage-split-slider:hover {
920
969
  box-shadow: 0 8px 20px rgba(27, 141, 127, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.8);
921
970
  border-color: rgba(27, 141, 127, 0.3);
922
971
  }
923
972
 
973
+ .mortgage-split-slider:hover::after {
974
+ transform: translateX(120%);
975
+ }
976
+
924
977
  .mortgage-split-slider.is-disabled {
925
978
  opacity: 0.55;
926
979
  filter: grayscale(0.4);
@@ -1098,6 +1151,17 @@
1098
1151
  box-shadow: 0 2px 6px rgba(27, 141, 127, 0.15), inset 0 1px 2px rgba(255, 255, 255, 0.6);
1099
1152
  transition: all 0.2s ease;
1100
1153
  letter-spacing: 0.2px;
1154
+ animation: split-center-breathe 2.6s ease-in-out infinite;
1155
+ }
1156
+
1157
+ @keyframes first-home-aurora {
1158
+ 0% { transform: translate3d(-5%, -12%, 0) scale(1); }
1159
+ 100% { transform: translate3d(6%, -6%, 0) scale(1.08); }
1160
+ }
1161
+
1162
+ @keyframes split-center-breathe {
1163
+ 0%, 100% { box-shadow: 0 2px 6px rgba(27, 141, 127, 0.15), inset 0 1px 2px rgba(255, 255, 255, 0.6); }
1164
+ 50% { box-shadow: 0 5px 14px rgba(27, 141, 127, 0.24), inset 0 1px 2px rgba(255, 255, 255, 0.75); }
1101
1165
  }
1102
1166
 
1103
1167
  #primaCasaMutuoSplitInfo {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mantenimento-app",
3
- "version": "2.4.1",
3
+ "version": "2.4.3",
4
4
  "description": "Frontend + backend architecture for the mantenimento calculator",
5
5
  "type": "commonjs",
6
6
  "main": "backend/calculate-model.js",