mantenimento-app 2.4.9 → 2.4.11

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
@@ -31,6 +31,7 @@ const defaultExpenseItems = [
31
31
  const EXPENSE_DETAIL_MAX_CHARS = 560;
32
32
  const EXPENSE_DETAIL_MAX_LINES = 10;
33
33
  const EXPENSE_DETAIL_MAX_ROWS = 8;
34
+ const EXPENSE_DETAIL_NOTE_SEPARATOR = "\n---\n";
34
35
 
35
36
  const QUOTA_MANTENIMENTO_PERC = 35;
36
37
 
@@ -256,6 +257,7 @@ const defaultExpenseItems = [
256
257
  expenseDetailColDue: "Scadenza",
257
258
  expenseDetailAddRow: "Aggiungi riga",
258
259
  expenseDetailRemoveRow: "Rimuovi riga",
260
+ expenseDetailFreeTextPlaceholder: "Note libere aggiuntive...",
259
261
  expenseDetailCharsRemaining: "Caratteri rimanenti: {count}",
260
262
  expenseRemoveTitle: "Rimuovi voce spesa",
261
263
  expenseRemoveBtn: "Rimuovi",
@@ -291,6 +293,8 @@ const defaultExpenseItems = [
291
293
  firstHomeLocativeValueHint: "Valore locativo mensile della casa assegnata, usato per valorizzare il beneficio economico implicito.",
292
294
  firstHomeMortgageAmountLabel: "Rata mutuo mensile ({currency})",
293
295
  firstHomeMortgageAmountHint: "Importo mensile complessivo della rata del mutuo prima casa.",
296
+ firstHomeMortgageExpiryLabel: "Scadenza mutuo",
297
+ firstHomeMortgageExpiryHint: "Data prevista di estinzione del mutuo prima casa.",
294
298
  firstHomeAssignedToLabel: "Casa assegnata a",
295
299
  firstHomeAssignedToHint: "Seleziona il coniuge a cui e ceduta la prima casa.",
296
300
  firstHomeAssignedToNone: "Nessuna cessione",
@@ -313,6 +317,7 @@ const defaultExpenseItems = [
313
317
  pdfPrimaryHomeNotDeclared: "Non dichiarato",
314
318
  pdfPrimaryHomeAssignedTo: "Assegnata a",
315
319
  pdfPrimaryHomeMonthlyAmount: "Rata mensile",
320
+ pdfPrimaryHomeExpiryDate: "Scadenza mutuo",
316
321
  pdfPrimaryHomeSplit: "Ripartizione mutuo",
317
322
  pdfPrimaryHomeAppliedOnlyColl: "Considerato solo se casa ceduta al collocatario.",
318
323
  pdfExtraordinaryRow: "Spese straordinarie (quota mensile da annuo)",
@@ -623,6 +628,7 @@ const defaultExpenseItems = [
623
628
  expenseDetailColDue: "Due date",
624
629
  expenseDetailAddRow: "Add row",
625
630
  expenseDetailRemoveRow: "Remove row",
631
+ expenseDetailFreeTextPlaceholder: "Additional free notes...",
626
632
  expenseDetailCharsRemaining: "Remaining characters: {count}",
627
633
  expenseRemoveTitle: "Remove expense item",
628
634
  expenseRemoveBtn: "Remove",
@@ -658,6 +664,8 @@ const defaultExpenseItems = [
658
664
  firstHomeLocativeValueHint: "Monthly rental value of the assigned home, used to value the implicit economic benefit.",
659
665
  firstHomeMortgageAmountLabel: "Monthly mortgage payment ({currency})",
660
666
  firstHomeMortgageAmountHint: "Total monthly amount of the primary-home mortgage payment.",
667
+ firstHomeMortgageExpiryLabel: "Mortgage expiry date",
668
+ firstHomeMortgageExpiryHint: "Expected payoff date of the primary-home mortgage.",
661
669
  firstHomeAssignedToLabel: "Home assigned to",
662
670
  firstHomeAssignedToHint: "Select which spouse receives assignment of the primary home.",
663
671
  firstHomeAssignedToNone: "No assignment",
@@ -680,6 +688,7 @@ const defaultExpenseItems = [
680
688
  pdfPrimaryHomeNotDeclared: "Not declared",
681
689
  pdfPrimaryHomeAssignedTo: "Assigned to",
682
690
  pdfPrimaryHomeMonthlyAmount: "Monthly payment",
691
+ pdfPrimaryHomeExpiryDate: "Mortgage expiry date",
683
692
  pdfPrimaryHomeSplit: "Mortgage split",
684
693
  pdfPrimaryHomeAppliedOnlyColl: "Counted only when the home is assigned to the custodial parent.",
685
694
  pdfExtraordinaryRow: "Extraordinary expenses (monthly share from yearly)",
@@ -1234,6 +1243,8 @@ const defaultExpenseItems = [
1234
1243
  const hintPrimaCasaValoreLocativo = document.getElementById("hintPrimaCasaValoreLocativo");
1235
1244
  const lblPrimaCasaMutuoImporto = document.getElementById("lblPrimaCasaMutuoImporto");
1236
1245
  const hintPrimaCasaMutuoImporto = document.getElementById("hintPrimaCasaMutuoImporto");
1246
+ const lblPrimaCasaMutuoScadenza = document.getElementById("lblPrimaCasaMutuoScadenza");
1247
+ const hintPrimaCasaMutuoScadenza = document.getElementById("hintPrimaCasaMutuoScadenza");
1237
1248
  const lblPrimaCasaAssegnataA = document.getElementById("lblPrimaCasaAssegnataA");
1238
1249
  const hintPrimaCasaAssegnataA = document.getElementById("hintPrimaCasaAssegnataA");
1239
1250
  const lblPrimaCasaMutuoPerc1 = document.getElementById("lblPrimaCasaMutuoPerc1");
@@ -1299,6 +1310,8 @@ const defaultExpenseItems = [
1299
1310
  if (hintPrimaCasaValoreLocativo) hintPrimaCasaValoreLocativo.title = tr("firstHomeLocativeValueHint");
1300
1311
  if (lblPrimaCasaMutuoImporto) lblPrimaCasaMutuoImporto.textContent = msg("firstHomeMortgageAmountLabel", { currency: currentCurrency });
1301
1312
  if (hintPrimaCasaMutuoImporto) hintPrimaCasaMutuoImporto.title = tr("firstHomeMortgageAmountHint");
1313
+ if (lblPrimaCasaMutuoScadenza) lblPrimaCasaMutuoScadenza.textContent = tr("firstHomeMortgageExpiryLabel");
1314
+ if (hintPrimaCasaMutuoScadenza) hintPrimaCasaMutuoScadenza.title = tr("firstHomeMortgageExpiryHint");
1302
1315
  if (lblPrimaCasaAssegnataA) lblPrimaCasaAssegnataA.textContent = tr("firstHomeAssignedToLabel");
1303
1316
  if (hintPrimaCasaAssegnataA) hintPrimaCasaAssegnataA.title = tr("firstHomeAssignedToHint");
1304
1317
  if (lblPrimaCasaMutuoPerc1) lblPrimaCasaMutuoPerc1.textContent = tr("firstHomeSplitLabel");
@@ -1421,6 +1434,18 @@ const defaultExpenseItems = [
1421
1434
  return `${short} ${currentCurrency}`;
1422
1435
  }
1423
1436
 
1437
+ function formatIsoDate(value) {
1438
+ const iso = String(value || "").trim();
1439
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(iso)) return "";
1440
+ const d = new Date(`${iso}T00:00:00`);
1441
+ if (Number.isNaN(d.getTime())) return iso;
1442
+ return new Intl.DateTimeFormat(getCurrentLocale(), {
1443
+ year: "numeric",
1444
+ month: "2-digit",
1445
+ day: "2-digit"
1446
+ }).format(d);
1447
+ }
1448
+
1424
1449
  function escapeHtml(value) {
1425
1450
  return String(value || "")
1426
1451
  .replaceAll("&", "&")
@@ -1471,25 +1496,48 @@ const defaultExpenseItems = [
1471
1496
  syncExpenseDetailTableFromStore(textarea);
1472
1497
  }
1473
1498
 
1474
- function parseExpenseDetailRows(raw) {
1475
- const lines = String(raw || "")
1499
+ function parseExpenseDetailPayload(raw) {
1500
+ const source = String(raw || "").trim();
1501
+ let tablePart = source;
1502
+ let notePart = "";
1503
+ const sepIdx = source.indexOf(EXPENSE_DETAIL_NOTE_SEPARATOR);
1504
+ if (sepIdx >= 0) {
1505
+ tablePart = source.slice(0, sepIdx).trim();
1506
+ notePart = source.slice(sepIdx + EXPENSE_DETAIL_NOTE_SEPARATOR.length).trim();
1507
+ }
1508
+
1509
+ const tableLines = tablePart
1476
1510
  .split(/\r?\n/)
1477
1511
  .map((line) => line.trim())
1478
1512
  .filter(Boolean);
1479
- if (!lines.length) return [{ what: "", amount: "", due: "" }];
1480
- return lines
1481
- .map((line) => {
1482
- const cols = line.split("|").map((part) => part.trim());
1483
- if (cols.length >= 2) {
1484
- return {
1485
- what: cols[0] || "",
1486
- amount: cols[1] || "",
1487
- due: cols.slice(2).join(" | ") || ""
1488
- };
1489
- }
1490
- return { what: line, amount: "", due: "" };
1491
- })
1492
- .slice(0, EXPENSE_DETAIL_MAX_ROWS);
1513
+ const rows = [];
1514
+ const looseNoteLines = [];
1515
+ tableLines.forEach((line) => {
1516
+ const cols = line.split("|").map((part) => part.trim());
1517
+ if (cols.length >= 2) {
1518
+ rows.push({
1519
+ what: cols[0] || "",
1520
+ amount: cols[1] || "",
1521
+ due: cols.slice(2).join(" | ") || ""
1522
+ });
1523
+ } else {
1524
+ looseNoteLines.push(line);
1525
+ }
1526
+ });
1527
+
1528
+ const resolvedNote = [notePart, looseNoteLines.join("\n")]
1529
+ .filter(Boolean)
1530
+ .join(notePart && looseNoteLines.length ? "\n" : "")
1531
+ .trim();
1532
+
1533
+ return {
1534
+ rows: (rows.length ? rows : [{ what: "", amount: "", due: "" }]).slice(0, EXPENSE_DETAIL_MAX_ROWS),
1535
+ note: resolvedNote
1536
+ };
1537
+ }
1538
+
1539
+ function parseExpenseDetailRows(raw) {
1540
+ return parseExpenseDetailPayload(raw).rows;
1493
1541
  }
1494
1542
 
1495
1543
  function sanitizeExpenseDetailCell(value) {
@@ -1510,7 +1558,14 @@ const defaultExpenseItems = [
1510
1558
  return compact.map((row) => `${row.what} | ${row.amount} | ${row.due}`.trim()).join("\n");
1511
1559
  }
1512
1560
 
1513
- function buildExpenseDetailTableHtml(textareaId, rows) {
1561
+ function serializeExpenseDetailPayload(rows, note) {
1562
+ const tablePart = serializeExpenseDetailRows(rows);
1563
+ const safeNote = String(note || "").trim();
1564
+ if (!safeNote) return tablePart;
1565
+ return `${tablePart}${tablePart ? EXPENSE_DETAIL_NOTE_SEPARATOR : ""}${safeNote}`;
1566
+ }
1567
+
1568
+ function buildExpenseDetailTableHtml(textareaId, rows, note = "") {
1514
1569
  const safeRows = (Array.isArray(rows) && rows.length ? rows : [{ what: "", amount: "", due: "" }])
1515
1570
  .slice(0, EXPENSE_DETAIL_MAX_ROWS);
1516
1571
  const rowsHtml = safeRows.map((row) => {
@@ -1535,6 +1590,7 @@ const defaultExpenseItems = [
1535
1590
  <tbody>${rowsHtml}</tbody>
1536
1591
  </table>
1537
1592
  <button type="button" class="btn-secondary spese-detail-add-row" data-row-add="${textareaId}">${escapeHtml(tr("expenseDetailAddRow"))}</button>
1593
+ <textarea class="spese-detail-note" data-detail-note-for="${textareaId}" rows="2" maxlength="360" placeholder="${escapeHtml(tr("expenseDetailFreeTextPlaceholder"))}">${escapeHtml(note || "")}</textarea>
1538
1594
  </div>`;
1539
1595
  }
1540
1596
 
@@ -1552,7 +1608,9 @@ const defaultExpenseItems = [
1552
1608
  const textareaId = String(tableWrap.getAttribute("data-detail-table") || "");
1553
1609
  const textarea = textareaId ? document.getElementById(textareaId) : null;
1554
1610
  if (!textarea) return;
1555
- const serialized = serializeExpenseDetailRows(readExpenseDetailRowsFromTable(tableWrap));
1611
+ const noteEl = tableWrap.querySelector(`textarea[data-detail-note-for='${textareaId}']`);
1612
+ const note = String(noteEl && noteEl.value ? noteEl.value : "");
1613
+ const serialized = serializeExpenseDetailPayload(readExpenseDetailRowsFromTable(tableWrap), note);
1556
1614
  textarea.value = serialized.slice(0, EXPENSE_DETAIL_MAX_CHARS);
1557
1615
  updateExpenseDetailCounter(textarea);
1558
1616
  }
@@ -1562,10 +1620,12 @@ const defaultExpenseItems = [
1562
1620
  const host = document.getElementById(`${textarea.id}TableHost`);
1563
1621
  if (!host) return;
1564
1622
  const existing = host.querySelector(`[data-detail-table='${textarea.id}']`);
1565
- const existingSerialized = serializeExpenseDetailRows(existing ? readExpenseDetailRowsFromTable(existing) : []);
1623
+ const existingNote = existing ? String(existing.querySelector(`textarea[data-detail-note-for='${textarea.id}']`)?.value || "") : "";
1624
+ const existingSerialized = serializeExpenseDetailPayload(existing ? readExpenseDetailRowsFromTable(existing) : [], existingNote);
1566
1625
  const storeSerialized = String(textarea.value || "").trim();
1567
1626
  if (existing && existingSerialized === storeSerialized) return;
1568
- host.innerHTML = buildExpenseDetailTableHtml(textarea.id, parseExpenseDetailRows(storeSerialized));
1627
+ const payload = parseExpenseDetailPayload(storeSerialized);
1628
+ host.innerHTML = buildExpenseDetailTableHtml(textarea.id, payload.rows, payload.note);
1569
1629
  syncExpenseDetailStoreFromTable(host.querySelector(`[data-detail-table='${textarea.id}']`));
1570
1630
  }
1571
1631
 
@@ -3724,6 +3784,7 @@ const defaultExpenseItems = [
3724
3784
  primaCasaMutuoEnabled: firstHome.enabled ? 1 : 0,
3725
3785
  primaCasaValoreLocativo: num("primaCasaValoreLocativo"),
3726
3786
  primaCasaMutuoImporto: firstHome.amount,
3787
+ primaCasaMutuoScadenza: firstHome.expiry,
3727
3788
  primaCasaAssegnataA: firstHome.assignedTo,
3728
3789
  primaCasaMutuoPerc1: firstHome.share1,
3729
3790
  straordAnn1: num("straordAnn1"),
@@ -3765,6 +3826,8 @@ const defaultExpenseItems = [
3765
3826
  const aFam2 = Number(payload.aFam2 || 0);
3766
3827
  const primaCasaMutuoEnabled = Number(payload.primaCasaMutuoEnabled || 0) > 0;
3767
3828
  const primaCasaMutuoImporto = Math.max(0, Number(payload.primaCasaMutuoImporto || 0));
3829
+ const primaCasaMutuoScadenzaRaw = String(payload.primaCasaMutuoScadenza || "").trim();
3830
+ const primaCasaMutuoScadenza = /^\d{4}-\d{2}-\d{2}$/.test(primaCasaMutuoScadenzaRaw) ? primaCasaMutuoScadenzaRaw : "";
3768
3831
  const primaCasaAssegnataA = (String(payload.primaCasaAssegnataA || "") === "1" || String(payload.primaCasaAssegnataA || "") === "2")
3769
3832
  ? String(payload.primaCasaAssegnataA)
3770
3833
  : "";
@@ -3904,7 +3967,7 @@ const defaultExpenseItems = [
3904
3967
  quotaDiretta1, quotaDiretta2,
3905
3968
  saldo1, saldo2,
3906
3969
  assegnoBaseDa1a2, assegnoBaseDa2a1,
3907
- primaCasaMutuoEnabled, primaCasaMutuoImporto, primaCasaAssegnataA, primaCasaValoreLocativo,
3970
+ primaCasaMutuoEnabled, primaCasaMutuoImporto, primaCasaMutuoScadenza, primaCasaAssegnataA, primaCasaValoreLocativo,
3908
3971
  primaCasaMutuoPerc1, primaCasaMutuoPerc2,
3909
3972
  primaCasaConsidered, primaCasaTransfer1to2, primaCasaTransfer2to1,
3910
3973
  compensativeBenefits,
@@ -4008,16 +4071,19 @@ const defaultExpenseItems = [
4008
4071
  function getFirstHomeMortgageInput() {
4009
4072
  const enabled = !!document.getElementById("primaCasaMutuoEnabled")?.checked;
4010
4073
  const amount = Math.max(0, num("primaCasaMutuoImporto"));
4074
+ const expiryRaw = String(document.getElementById("primaCasaMutuoScadenza")?.value || "").trim();
4075
+ const expiry = /^\d{4}-\d{2}-\d{2}$/.test(expiryRaw) ? expiryRaw : "";
4011
4076
  const assignedToRaw = String(document.getElementById("primaCasaAssegnataA")?.value || "").trim();
4012
4077
  const assignedTo = (assignedToRaw === "1" || assignedToRaw === "2") ? assignedToRaw : "";
4013
4078
  const share1 = Math.min(100, Math.max(0, num("primaCasaMutuoPerc1")));
4014
4079
  const share2 = 100 - share1;
4015
- return { enabled, amount, assignedTo, share1, share2 };
4080
+ return { enabled, amount, expiry, assignedTo, share1, share2 };
4016
4081
  }
4017
4082
 
4018
4083
  function updateFirstHomeMortgageUi() {
4019
4084
  const enabledEl = document.getElementById("primaCasaMutuoEnabled");
4020
4085
  const amountEl = document.getElementById("primaCasaMutuoImporto");
4086
+ const expiryEl = document.getElementById("primaCasaMutuoScadenza");
4021
4087
  const assignedEl = document.getElementById("primaCasaAssegnataA");
4022
4088
  const shareEl = document.getElementById("primaCasaMutuoPerc1");
4023
4089
  const splitInfoEl = document.getElementById("primaCasaMutuoSplitInfo");
@@ -4033,6 +4099,7 @@ const defaultExpenseItems = [
4033
4099
 
4034
4100
  const isEnabled = !!enabledEl.checked;
4035
4101
  amountEl.disabled = !isEnabled;
4102
+ if (expiryEl) expiryEl.disabled = !isEnabled;
4036
4103
  assignedEl.disabled = !isEnabled;
4037
4104
  shareEl.disabled = !isEnabled;
4038
4105
  if (splitWrapEl) splitWrapEl.classList.toggle("is-disabled", !isEnabled);
@@ -4563,6 +4630,7 @@ const defaultExpenseItems = [
4563
4630
  setVal("assegnoFam2", Number(payload.aFam2 || 0));
4564
4631
  setChecked("primaCasaMutuoEnabled", Number(payload.primaCasaMutuoEnabled || 0) > 0);
4565
4632
  setVal("primaCasaMutuoImporto", Number(payload.primaCasaMutuoImporto || 0));
4633
+ setVal("primaCasaMutuoScadenza", String(payload.primaCasaMutuoScadenza || ""));
4566
4634
  setVal("primaCasaAssegnataA", (String(payload.primaCasaAssegnataA || "") === "1" || String(payload.primaCasaAssegnataA || "") === "2") ? String(payload.primaCasaAssegnataA) : "");
4567
4635
  setVal("primaCasaMutuoPerc1", Math.min(100, Math.max(0, Number((payload.primaCasaMutuoPerc1 === undefined ? 50 : payload.primaCasaMutuoPerc1) || 0))));
4568
4636
  setVal("straordAnn1", Number(payload.straordAnn1 || 0));
@@ -4737,6 +4805,7 @@ const defaultExpenseItems = [
4737
4805
  ? `
4738
4806
  <tr><td>${tr("pdfPrimaryHomeAssignedTo")}</td><td>${primaryHomeAssignedLabel}</td></tr>
4739
4807
  <tr><td>${tr("pdfPrimaryHomeMonthlyAmount")}</td><td>${eur(m.primaCasaMutuoImporto || 0)}</td></tr>
4808
+ <tr><td>${tr("pdfPrimaryHomeExpiryDate")}</td><td>${escapeHtml(formatIsoDate(m.primaCasaMutuoScadenza) || tr("pdfPrimaryHomeNotDeclared"))}</td></tr>
4740
4809
  <tr><td>${tr("pdfPrimaryHomeSplit")}</td><td>${c1NameEsc} ${(m.primaCasaMutuoPerc1 || 0).toFixed(0)}% · ${c2NameEsc} ${(m.primaCasaMutuoPerc2 || 0).toFixed(0)}%</td></tr>
4741
4810
  <tr><td>${tr("pdfPrimaryHomeAppliedOnlyColl")}</td><td>${m.primaCasaConsidered ? "OK" : tr("pdfPrimaryHomeNotDeclared")}</td></tr>`
4742
4811
  : `<tr><td>${tr("pdfPrimaryHomeMortgage")}</td><td>${tr("pdfPrimaryHomeNotDeclared")}</td></tr>`;
@@ -5255,6 +5324,7 @@ const defaultExpenseItems = [
5255
5324
  ? `
5256
5325
  <tr><td>${tr("pdfPrimaryHomeAssignedTo")}</td><td>${primaryHomeAssignedLabel}</td></tr>
5257
5326
  <tr><td>${tr("pdfPrimaryHomeMonthlyAmount")}</td><td>${eur(m.primaCasaMutuoImporto || 0)}</td></tr>
5327
+ <tr><td>${tr("pdfPrimaryHomeExpiryDate")}</td><td>${escapeHtml(formatIsoDate(m.primaCasaMutuoScadenza) || tr("pdfPrimaryHomeNotDeclared"))}</td></tr>
5258
5328
  <tr><td>${tr("pdfPrimaryHomeSplit")}</td><td>${c1NameEsc} ${(m.primaCasaMutuoPerc1 || 0).toFixed(0)}% · ${c2NameEsc} ${(m.primaCasaMutuoPerc2 || 0).toFixed(0)}%</td></tr>
5259
5329
  <tr><td>${tr("pdfPrimaryHomeAppliedOnlyColl")}</td><td>${m.primaCasaConsidered ? "OK" : tr("pdfPrimaryHomeNotDeclared")}</td></tr>`
5260
5330
  : `<tr><td>${tr("pdfPrimaryHomeMortgage")}</td><td>${tr("pdfPrimaryHomeNotDeclared")}</td></tr>`;
@@ -6163,6 +6233,7 @@ ${scenarioLab.length ? `
6163
6233
  primaCasaMutuoEnabled: document.getElementById("primaCasaMutuoEnabled")?.checked ? 1 : 0,
6164
6234
  primaCasaValoreLocativo: num("primaCasaValoreLocativo"),
6165
6235
  primaCasaMutuoImporto: num("primaCasaMutuoImporto"),
6236
+ primaCasaMutuoScadenza: String(document.getElementById("primaCasaMutuoScadenza")?.value || ""),
6166
6237
  primaCasaAssegnataA: String(document.getElementById("primaCasaAssegnataA")?.value || ""),
6167
6238
  primaCasaMutuoPerc1: num("primaCasaMutuoPerc1"),
6168
6239
  straordAnn1: num("straordAnn1"),
@@ -6290,8 +6361,10 @@ ${scenarioLab.length ? `
6290
6361
  });
6291
6362
  const firstHomeEnabled = document.getElementById("primaCasaMutuoEnabled");
6292
6363
  const firstHomeAssigned = document.getElementById("primaCasaAssegnataA");
6364
+ const firstHomeExpiry = document.getElementById("primaCasaMutuoScadenza");
6293
6365
  if (firstHomeEnabled) firstHomeEnabled.checked = !!firstHomeEnabled.defaultChecked;
6294
6366
  if (firstHomeAssigned) firstHomeAssigned.value = "";
6367
+ if (firstHomeExpiry) firstHomeExpiry.value = firstHomeExpiry.defaultValue || "";
6295
6368
  permanenceCalendarState.byMonth = {};
6296
6369
  speseConvivenzaAutoMode = true;
6297
6370
  selectedScenarioIdx = -1;
@@ -6446,10 +6519,11 @@ ${scenarioLab.length ? `
6446
6519
  const textareaId = String(addRowBtn.getAttribute("data-row-add") || "");
6447
6520
  const textarea = textareaId ? document.getElementById(textareaId) : null;
6448
6521
  if (!textarea) return;
6449
- const rows = parseExpenseDetailRows(textarea.value);
6522
+ const payload = parseExpenseDetailPayload(textarea.value);
6523
+ const rows = payload.rows;
6450
6524
  if (rows.length < EXPENSE_DETAIL_MAX_ROWS) {
6451
6525
  rows.push({ what: "", amount: "", due: "" });
6452
- textarea.value = serializeExpenseDetailRows(rows);
6526
+ textarea.value = serializeExpenseDetailPayload(rows, payload.note);
6453
6527
  syncExpenseDetailTableFromStore(textarea);
6454
6528
  refreshExpenseDetailButtonState();
6455
6529
  }
@@ -6491,6 +6565,12 @@ ${scenarioLab.length ? `
6491
6565
  const tableWrap = e.target.closest("[data-detail-table]");
6492
6566
  syncExpenseDetailStoreFromTable(tableWrap);
6493
6567
  refreshExpenseDetailButtonState();
6568
+ return;
6569
+ }
6570
+ if (e.target && e.target.matches(".spese-detail-note")) {
6571
+ const tableWrap = e.target.closest("[data-detail-table]");
6572
+ syncExpenseDetailStoreFromTable(tableWrap);
6573
+ refreshExpenseDetailButtonState();
6494
6574
  }
6495
6575
  });
6496
6576
 
@@ -6630,7 +6710,7 @@ ${scenarioLab.length ? `
6630
6710
  }
6631
6711
  updateModeUi();
6632
6712
  renderAll();
6633
- } else if (e.target && (e.target.id === "primaCasaMutuoEnabled" || e.target.id === "primaCasaAssegnataA")) {
6713
+ } else if (e.target && (e.target.id === "primaCasaMutuoEnabled" || e.target.id === "primaCasaAssegnataA" || e.target.id === "primaCasaMutuoScadenza")) {
6634
6714
  updateFirstHomeMortgageUi();
6635
6715
  renderAll();
6636
6716
  }
@@ -35,6 +35,8 @@ function calculateModel(input) {
35
35
  const primaCasaMutuoEnabled = toNumber(input.primaCasaMutuoEnabled) > 0;
36
36
  const primaCasaValoreLocativo = Math.max(0, toNumber(input.primaCasaValoreLocativo));
37
37
  const primaCasaMutuoImporto = Math.max(0, toNumber(input.primaCasaMutuoImporto));
38
+ const primaCasaMutuoScadenzaRaw = String(input.primaCasaMutuoScadenza || '').trim();
39
+ const primaCasaMutuoScadenza = /^\d{4}-\d{2}-\d{2}$/.test(primaCasaMutuoScadenzaRaw) ? primaCasaMutuoScadenzaRaw : '';
38
40
  const primaCasaAssegnataA = String(input.primaCasaAssegnataA || '');
39
41
  const rawMutuoPerc1 = input.primaCasaMutuoPerc1 === undefined ? 50 : input.primaCasaMutuoPerc1;
40
42
  const primaCasaMutuoPerc1 = clamp(toNumber(rawMutuoPerc1), 0, 100);
@@ -163,7 +165,7 @@ function calculateModel(input) {
163
165
  quotaDiretta1, quotaDiretta2,
164
166
  saldo1, saldo2,
165
167
  assegnoBaseDa1a2, assegnoBaseDa2a1,
166
- primaCasaMutuoEnabled, primaCasaValoreLocativo, primaCasaMutuoImporto,
168
+ primaCasaMutuoEnabled, primaCasaValoreLocativo, primaCasaMutuoImporto, primaCasaMutuoScadenza,
167
169
  primaCasaAssegnataA: assigned,
168
170
  primaCasaMutuoPerc1, primaCasaMutuoPerc2,
169
171
  primaCasaConsidered, primaCasaTransfer1to2, primaCasaTransfer2to1,
@@ -31,6 +31,7 @@ const defaultExpenseItems = [
31
31
  const EXPENSE_DETAIL_MAX_CHARS = 560;
32
32
  const EXPENSE_DETAIL_MAX_LINES = 10;
33
33
  const EXPENSE_DETAIL_MAX_ROWS = 8;
34
+ const EXPENSE_DETAIL_NOTE_SEPARATOR = "\n---\n";
34
35
 
35
36
  const QUOTA_MANTENIMENTO_PERC = 35;
36
37
 
@@ -256,6 +257,7 @@ const defaultExpenseItems = [
256
257
  expenseDetailColDue: "Scadenza",
257
258
  expenseDetailAddRow: "Aggiungi riga",
258
259
  expenseDetailRemoveRow: "Rimuovi riga",
260
+ expenseDetailFreeTextPlaceholder: "Note libere aggiuntive...",
259
261
  expenseDetailCharsRemaining: "Caratteri rimanenti: {count}",
260
262
  expenseRemoveTitle: "Rimuovi voce spesa",
261
263
  expenseRemoveBtn: "Rimuovi",
@@ -291,6 +293,8 @@ const defaultExpenseItems = [
291
293
  firstHomeLocativeValueHint: "Valore locativo mensile della casa assegnata, usato per valorizzare il beneficio economico implicito.",
292
294
  firstHomeMortgageAmountLabel: "Rata mutuo mensile ({currency})",
293
295
  firstHomeMortgageAmountHint: "Importo mensile complessivo della rata del mutuo prima casa.",
296
+ firstHomeMortgageExpiryLabel: "Scadenza mutuo",
297
+ firstHomeMortgageExpiryHint: "Data prevista di estinzione del mutuo prima casa.",
294
298
  firstHomeAssignedToLabel: "Casa assegnata a",
295
299
  firstHomeAssignedToHint: "Seleziona il coniuge a cui e ceduta la prima casa.",
296
300
  firstHomeAssignedToNone: "Nessuna cessione",
@@ -313,6 +317,7 @@ const defaultExpenseItems = [
313
317
  pdfPrimaryHomeNotDeclared: "Non dichiarato",
314
318
  pdfPrimaryHomeAssignedTo: "Assegnata a",
315
319
  pdfPrimaryHomeMonthlyAmount: "Rata mensile",
320
+ pdfPrimaryHomeExpiryDate: "Scadenza mutuo",
316
321
  pdfPrimaryHomeSplit: "Ripartizione mutuo",
317
322
  pdfPrimaryHomeAppliedOnlyColl: "Considerato solo se casa ceduta al collocatario.",
318
323
  pdfExtraordinaryRow: "Spese straordinarie (quota mensile da annuo)",
@@ -623,6 +628,7 @@ const defaultExpenseItems = [
623
628
  expenseDetailColDue: "Due date",
624
629
  expenseDetailAddRow: "Add row",
625
630
  expenseDetailRemoveRow: "Remove row",
631
+ expenseDetailFreeTextPlaceholder: "Additional free notes...",
626
632
  expenseDetailCharsRemaining: "Remaining characters: {count}",
627
633
  expenseRemoveTitle: "Remove expense item",
628
634
  expenseRemoveBtn: "Remove",
@@ -658,6 +664,8 @@ const defaultExpenseItems = [
658
664
  firstHomeLocativeValueHint: "Monthly rental value of the assigned home, used to value the implicit economic benefit.",
659
665
  firstHomeMortgageAmountLabel: "Monthly mortgage payment ({currency})",
660
666
  firstHomeMortgageAmountHint: "Total monthly amount of the primary-home mortgage payment.",
667
+ firstHomeMortgageExpiryLabel: "Mortgage expiry date",
668
+ firstHomeMortgageExpiryHint: "Expected payoff date of the primary-home mortgage.",
661
669
  firstHomeAssignedToLabel: "Home assigned to",
662
670
  firstHomeAssignedToHint: "Select which spouse receives assignment of the primary home.",
663
671
  firstHomeAssignedToNone: "No assignment",
@@ -680,6 +688,7 @@ const defaultExpenseItems = [
680
688
  pdfPrimaryHomeNotDeclared: "Not declared",
681
689
  pdfPrimaryHomeAssignedTo: "Assigned to",
682
690
  pdfPrimaryHomeMonthlyAmount: "Monthly payment",
691
+ pdfPrimaryHomeExpiryDate: "Mortgage expiry date",
683
692
  pdfPrimaryHomeSplit: "Mortgage split",
684
693
  pdfPrimaryHomeAppliedOnlyColl: "Counted only when the home is assigned to the custodial parent.",
685
694
  pdfExtraordinaryRow: "Extraordinary expenses (monthly share from yearly)",
@@ -1234,6 +1243,8 @@ const defaultExpenseItems = [
1234
1243
  const hintPrimaCasaValoreLocativo = document.getElementById("hintPrimaCasaValoreLocativo");
1235
1244
  const lblPrimaCasaMutuoImporto = document.getElementById("lblPrimaCasaMutuoImporto");
1236
1245
  const hintPrimaCasaMutuoImporto = document.getElementById("hintPrimaCasaMutuoImporto");
1246
+ const lblPrimaCasaMutuoScadenza = document.getElementById("lblPrimaCasaMutuoScadenza");
1247
+ const hintPrimaCasaMutuoScadenza = document.getElementById("hintPrimaCasaMutuoScadenza");
1237
1248
  const lblPrimaCasaAssegnataA = document.getElementById("lblPrimaCasaAssegnataA");
1238
1249
  const hintPrimaCasaAssegnataA = document.getElementById("hintPrimaCasaAssegnataA");
1239
1250
  const lblPrimaCasaMutuoPerc1 = document.getElementById("lblPrimaCasaMutuoPerc1");
@@ -1299,6 +1310,8 @@ const defaultExpenseItems = [
1299
1310
  if (hintPrimaCasaValoreLocativo) hintPrimaCasaValoreLocativo.title = tr("firstHomeLocativeValueHint");
1300
1311
  if (lblPrimaCasaMutuoImporto) lblPrimaCasaMutuoImporto.textContent = msg("firstHomeMortgageAmountLabel", { currency: currentCurrency });
1301
1312
  if (hintPrimaCasaMutuoImporto) hintPrimaCasaMutuoImporto.title = tr("firstHomeMortgageAmountHint");
1313
+ if (lblPrimaCasaMutuoScadenza) lblPrimaCasaMutuoScadenza.textContent = tr("firstHomeMortgageExpiryLabel");
1314
+ if (hintPrimaCasaMutuoScadenza) hintPrimaCasaMutuoScadenza.title = tr("firstHomeMortgageExpiryHint");
1302
1315
  if (lblPrimaCasaAssegnataA) lblPrimaCasaAssegnataA.textContent = tr("firstHomeAssignedToLabel");
1303
1316
  if (hintPrimaCasaAssegnataA) hintPrimaCasaAssegnataA.title = tr("firstHomeAssignedToHint");
1304
1317
  if (lblPrimaCasaMutuoPerc1) lblPrimaCasaMutuoPerc1.textContent = tr("firstHomeSplitLabel");
@@ -1421,6 +1434,18 @@ const defaultExpenseItems = [
1421
1434
  return `${short} ${currentCurrency}`;
1422
1435
  }
1423
1436
 
1437
+ function formatIsoDate(value) {
1438
+ const iso = String(value || "").trim();
1439
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(iso)) return "";
1440
+ const d = new Date(`${iso}T00:00:00`);
1441
+ if (Number.isNaN(d.getTime())) return iso;
1442
+ return new Intl.DateTimeFormat(getCurrentLocale(), {
1443
+ year: "numeric",
1444
+ month: "2-digit",
1445
+ day: "2-digit"
1446
+ }).format(d);
1447
+ }
1448
+
1424
1449
  function escapeHtml(value) {
1425
1450
  return String(value || "")
1426
1451
  .replaceAll("&", "&amp;")
@@ -1471,25 +1496,48 @@ const defaultExpenseItems = [
1471
1496
  syncExpenseDetailTableFromStore(textarea);
1472
1497
  }
1473
1498
 
1474
- function parseExpenseDetailRows(raw) {
1475
- const lines = String(raw || "")
1499
+ function parseExpenseDetailPayload(raw) {
1500
+ const source = String(raw || "").trim();
1501
+ let tablePart = source;
1502
+ let notePart = "";
1503
+ const sepIdx = source.indexOf(EXPENSE_DETAIL_NOTE_SEPARATOR);
1504
+ if (sepIdx >= 0) {
1505
+ tablePart = source.slice(0, sepIdx).trim();
1506
+ notePart = source.slice(sepIdx + EXPENSE_DETAIL_NOTE_SEPARATOR.length).trim();
1507
+ }
1508
+
1509
+ const tableLines = tablePart
1476
1510
  .split(/\r?\n/)
1477
1511
  .map((line) => line.trim())
1478
1512
  .filter(Boolean);
1479
- if (!lines.length) return [{ what: "", amount: "", due: "" }];
1480
- return lines
1481
- .map((line) => {
1482
- const cols = line.split("|").map((part) => part.trim());
1483
- if (cols.length >= 2) {
1484
- return {
1485
- what: cols[0] || "",
1486
- amount: cols[1] || "",
1487
- due: cols.slice(2).join(" | ") || ""
1488
- };
1489
- }
1490
- return { what: line, amount: "", due: "" };
1491
- })
1492
- .slice(0, EXPENSE_DETAIL_MAX_ROWS);
1513
+ const rows = [];
1514
+ const looseNoteLines = [];
1515
+ tableLines.forEach((line) => {
1516
+ const cols = line.split("|").map((part) => part.trim());
1517
+ if (cols.length >= 2) {
1518
+ rows.push({
1519
+ what: cols[0] || "",
1520
+ amount: cols[1] || "",
1521
+ due: cols.slice(2).join(" | ") || ""
1522
+ });
1523
+ } else {
1524
+ looseNoteLines.push(line);
1525
+ }
1526
+ });
1527
+
1528
+ const resolvedNote = [notePart, looseNoteLines.join("\n")]
1529
+ .filter(Boolean)
1530
+ .join(notePart && looseNoteLines.length ? "\n" : "")
1531
+ .trim();
1532
+
1533
+ return {
1534
+ rows: (rows.length ? rows : [{ what: "", amount: "", due: "" }]).slice(0, EXPENSE_DETAIL_MAX_ROWS),
1535
+ note: resolvedNote
1536
+ };
1537
+ }
1538
+
1539
+ function parseExpenseDetailRows(raw) {
1540
+ return parseExpenseDetailPayload(raw).rows;
1493
1541
  }
1494
1542
 
1495
1543
  function sanitizeExpenseDetailCell(value) {
@@ -1510,7 +1558,14 @@ const defaultExpenseItems = [
1510
1558
  return compact.map((row) => `${row.what} | ${row.amount} | ${row.due}`.trim()).join("\n");
1511
1559
  }
1512
1560
 
1513
- function buildExpenseDetailTableHtml(textareaId, rows) {
1561
+ function serializeExpenseDetailPayload(rows, note) {
1562
+ const tablePart = serializeExpenseDetailRows(rows);
1563
+ const safeNote = String(note || "").trim();
1564
+ if (!safeNote) return tablePart;
1565
+ return `${tablePart}${tablePart ? EXPENSE_DETAIL_NOTE_SEPARATOR : ""}${safeNote}`;
1566
+ }
1567
+
1568
+ function buildExpenseDetailTableHtml(textareaId, rows, note = "") {
1514
1569
  const safeRows = (Array.isArray(rows) && rows.length ? rows : [{ what: "", amount: "", due: "" }])
1515
1570
  .slice(0, EXPENSE_DETAIL_MAX_ROWS);
1516
1571
  const rowsHtml = safeRows.map((row) => {
@@ -1535,6 +1590,7 @@ const defaultExpenseItems = [
1535
1590
  <tbody>${rowsHtml}</tbody>
1536
1591
  </table>
1537
1592
  <button type="button" class="btn-secondary spese-detail-add-row" data-row-add="${textareaId}">${escapeHtml(tr("expenseDetailAddRow"))}</button>
1593
+ <textarea class="spese-detail-note" data-detail-note-for="${textareaId}" rows="2" maxlength="360" placeholder="${escapeHtml(tr("expenseDetailFreeTextPlaceholder"))}">${escapeHtml(note || "")}</textarea>
1538
1594
  </div>`;
1539
1595
  }
1540
1596
 
@@ -1552,7 +1608,9 @@ const defaultExpenseItems = [
1552
1608
  const textareaId = String(tableWrap.getAttribute("data-detail-table") || "");
1553
1609
  const textarea = textareaId ? document.getElementById(textareaId) : null;
1554
1610
  if (!textarea) return;
1555
- const serialized = serializeExpenseDetailRows(readExpenseDetailRowsFromTable(tableWrap));
1611
+ const noteEl = tableWrap.querySelector(`textarea[data-detail-note-for='${textareaId}']`);
1612
+ const note = String(noteEl && noteEl.value ? noteEl.value : "");
1613
+ const serialized = serializeExpenseDetailPayload(readExpenseDetailRowsFromTable(tableWrap), note);
1556
1614
  textarea.value = serialized.slice(0, EXPENSE_DETAIL_MAX_CHARS);
1557
1615
  updateExpenseDetailCounter(textarea);
1558
1616
  }
@@ -1562,10 +1620,12 @@ const defaultExpenseItems = [
1562
1620
  const host = document.getElementById(`${textarea.id}TableHost`);
1563
1621
  if (!host) return;
1564
1622
  const existing = host.querySelector(`[data-detail-table='${textarea.id}']`);
1565
- const existingSerialized = serializeExpenseDetailRows(existing ? readExpenseDetailRowsFromTable(existing) : []);
1623
+ const existingNote = existing ? String(existing.querySelector(`textarea[data-detail-note-for='${textarea.id}']`)?.value || "") : "";
1624
+ const existingSerialized = serializeExpenseDetailPayload(existing ? readExpenseDetailRowsFromTable(existing) : [], existingNote);
1566
1625
  const storeSerialized = String(textarea.value || "").trim();
1567
1626
  if (existing && existingSerialized === storeSerialized) return;
1568
- host.innerHTML = buildExpenseDetailTableHtml(textarea.id, parseExpenseDetailRows(storeSerialized));
1627
+ const payload = parseExpenseDetailPayload(storeSerialized);
1628
+ host.innerHTML = buildExpenseDetailTableHtml(textarea.id, payload.rows, payload.note);
1569
1629
  syncExpenseDetailStoreFromTable(host.querySelector(`[data-detail-table='${textarea.id}']`));
1570
1630
  }
1571
1631
 
@@ -3724,6 +3784,7 @@ const defaultExpenseItems = [
3724
3784
  primaCasaMutuoEnabled: firstHome.enabled ? 1 : 0,
3725
3785
  primaCasaValoreLocativo: num("primaCasaValoreLocativo"),
3726
3786
  primaCasaMutuoImporto: firstHome.amount,
3787
+ primaCasaMutuoScadenza: firstHome.expiry,
3727
3788
  primaCasaAssegnataA: firstHome.assignedTo,
3728
3789
  primaCasaMutuoPerc1: firstHome.share1,
3729
3790
  straordAnn1: num("straordAnn1"),
@@ -3765,6 +3826,8 @@ const defaultExpenseItems = [
3765
3826
  const aFam2 = Number(payload.aFam2 || 0);
3766
3827
  const primaCasaMutuoEnabled = Number(payload.primaCasaMutuoEnabled || 0) > 0;
3767
3828
  const primaCasaMutuoImporto = Math.max(0, Number(payload.primaCasaMutuoImporto || 0));
3829
+ const primaCasaMutuoScadenzaRaw = String(payload.primaCasaMutuoScadenza || "").trim();
3830
+ const primaCasaMutuoScadenza = /^\d{4}-\d{2}-\d{2}$/.test(primaCasaMutuoScadenzaRaw) ? primaCasaMutuoScadenzaRaw : "";
3768
3831
  const primaCasaAssegnataA = (String(payload.primaCasaAssegnataA || "") === "1" || String(payload.primaCasaAssegnataA || "") === "2")
3769
3832
  ? String(payload.primaCasaAssegnataA)
3770
3833
  : "";
@@ -3904,7 +3967,7 @@ const defaultExpenseItems = [
3904
3967
  quotaDiretta1, quotaDiretta2,
3905
3968
  saldo1, saldo2,
3906
3969
  assegnoBaseDa1a2, assegnoBaseDa2a1,
3907
- primaCasaMutuoEnabled, primaCasaMutuoImporto, primaCasaAssegnataA, primaCasaValoreLocativo,
3970
+ primaCasaMutuoEnabled, primaCasaMutuoImporto, primaCasaMutuoScadenza, primaCasaAssegnataA, primaCasaValoreLocativo,
3908
3971
  primaCasaMutuoPerc1, primaCasaMutuoPerc2,
3909
3972
  primaCasaConsidered, primaCasaTransfer1to2, primaCasaTransfer2to1,
3910
3973
  compensativeBenefits,
@@ -4008,16 +4071,19 @@ const defaultExpenseItems = [
4008
4071
  function getFirstHomeMortgageInput() {
4009
4072
  const enabled = !!document.getElementById("primaCasaMutuoEnabled")?.checked;
4010
4073
  const amount = Math.max(0, num("primaCasaMutuoImporto"));
4074
+ const expiryRaw = String(document.getElementById("primaCasaMutuoScadenza")?.value || "").trim();
4075
+ const expiry = /^\d{4}-\d{2}-\d{2}$/.test(expiryRaw) ? expiryRaw : "";
4011
4076
  const assignedToRaw = String(document.getElementById("primaCasaAssegnataA")?.value || "").trim();
4012
4077
  const assignedTo = (assignedToRaw === "1" || assignedToRaw === "2") ? assignedToRaw : "";
4013
4078
  const share1 = Math.min(100, Math.max(0, num("primaCasaMutuoPerc1")));
4014
4079
  const share2 = 100 - share1;
4015
- return { enabled, amount, assignedTo, share1, share2 };
4080
+ return { enabled, amount, expiry, assignedTo, share1, share2 };
4016
4081
  }
4017
4082
 
4018
4083
  function updateFirstHomeMortgageUi() {
4019
4084
  const enabledEl = document.getElementById("primaCasaMutuoEnabled");
4020
4085
  const amountEl = document.getElementById("primaCasaMutuoImporto");
4086
+ const expiryEl = document.getElementById("primaCasaMutuoScadenza");
4021
4087
  const assignedEl = document.getElementById("primaCasaAssegnataA");
4022
4088
  const shareEl = document.getElementById("primaCasaMutuoPerc1");
4023
4089
  const splitInfoEl = document.getElementById("primaCasaMutuoSplitInfo");
@@ -4033,6 +4099,7 @@ const defaultExpenseItems = [
4033
4099
 
4034
4100
  const isEnabled = !!enabledEl.checked;
4035
4101
  amountEl.disabled = !isEnabled;
4102
+ if (expiryEl) expiryEl.disabled = !isEnabled;
4036
4103
  assignedEl.disabled = !isEnabled;
4037
4104
  shareEl.disabled = !isEnabled;
4038
4105
  if (splitWrapEl) splitWrapEl.classList.toggle("is-disabled", !isEnabled);
@@ -4563,6 +4630,7 @@ const defaultExpenseItems = [
4563
4630
  setVal("assegnoFam2", Number(payload.aFam2 || 0));
4564
4631
  setChecked("primaCasaMutuoEnabled", Number(payload.primaCasaMutuoEnabled || 0) > 0);
4565
4632
  setVal("primaCasaMutuoImporto", Number(payload.primaCasaMutuoImporto || 0));
4633
+ setVal("primaCasaMutuoScadenza", String(payload.primaCasaMutuoScadenza || ""));
4566
4634
  setVal("primaCasaAssegnataA", (String(payload.primaCasaAssegnataA || "") === "1" || String(payload.primaCasaAssegnataA || "") === "2") ? String(payload.primaCasaAssegnataA) : "");
4567
4635
  setVal("primaCasaMutuoPerc1", Math.min(100, Math.max(0, Number((payload.primaCasaMutuoPerc1 === undefined ? 50 : payload.primaCasaMutuoPerc1) || 0))));
4568
4636
  setVal("straordAnn1", Number(payload.straordAnn1 || 0));
@@ -4737,6 +4805,7 @@ const defaultExpenseItems = [
4737
4805
  ? `
4738
4806
  <tr><td>${tr("pdfPrimaryHomeAssignedTo")}</td><td>${primaryHomeAssignedLabel}</td></tr>
4739
4807
  <tr><td>${tr("pdfPrimaryHomeMonthlyAmount")}</td><td>${eur(m.primaCasaMutuoImporto || 0)}</td></tr>
4808
+ <tr><td>${tr("pdfPrimaryHomeExpiryDate")}</td><td>${escapeHtml(formatIsoDate(m.primaCasaMutuoScadenza) || tr("pdfPrimaryHomeNotDeclared"))}</td></tr>
4740
4809
  <tr><td>${tr("pdfPrimaryHomeSplit")}</td><td>${c1NameEsc} ${(m.primaCasaMutuoPerc1 || 0).toFixed(0)}% · ${c2NameEsc} ${(m.primaCasaMutuoPerc2 || 0).toFixed(0)}%</td></tr>
4741
4810
  <tr><td>${tr("pdfPrimaryHomeAppliedOnlyColl")}</td><td>${m.primaCasaConsidered ? "OK" : tr("pdfPrimaryHomeNotDeclared")}</td></tr>`
4742
4811
  : `<tr><td>${tr("pdfPrimaryHomeMortgage")}</td><td>${tr("pdfPrimaryHomeNotDeclared")}</td></tr>`;
@@ -5255,6 +5324,7 @@ const defaultExpenseItems = [
5255
5324
  ? `
5256
5325
  <tr><td>${tr("pdfPrimaryHomeAssignedTo")}</td><td>${primaryHomeAssignedLabel}</td></tr>
5257
5326
  <tr><td>${tr("pdfPrimaryHomeMonthlyAmount")}</td><td>${eur(m.primaCasaMutuoImporto || 0)}</td></tr>
5327
+ <tr><td>${tr("pdfPrimaryHomeExpiryDate")}</td><td>${escapeHtml(formatIsoDate(m.primaCasaMutuoScadenza) || tr("pdfPrimaryHomeNotDeclared"))}</td></tr>
5258
5328
  <tr><td>${tr("pdfPrimaryHomeSplit")}</td><td>${c1NameEsc} ${(m.primaCasaMutuoPerc1 || 0).toFixed(0)}% · ${c2NameEsc} ${(m.primaCasaMutuoPerc2 || 0).toFixed(0)}%</td></tr>
5259
5329
  <tr><td>${tr("pdfPrimaryHomeAppliedOnlyColl")}</td><td>${m.primaCasaConsidered ? "OK" : tr("pdfPrimaryHomeNotDeclared")}</td></tr>`
5260
5330
  : `<tr><td>${tr("pdfPrimaryHomeMortgage")}</td><td>${tr("pdfPrimaryHomeNotDeclared")}</td></tr>`;
@@ -6163,6 +6233,7 @@ ${scenarioLab.length ? `
6163
6233
  primaCasaMutuoEnabled: document.getElementById("primaCasaMutuoEnabled")?.checked ? 1 : 0,
6164
6234
  primaCasaValoreLocativo: num("primaCasaValoreLocativo"),
6165
6235
  primaCasaMutuoImporto: num("primaCasaMutuoImporto"),
6236
+ primaCasaMutuoScadenza: String(document.getElementById("primaCasaMutuoScadenza")?.value || ""),
6166
6237
  primaCasaAssegnataA: String(document.getElementById("primaCasaAssegnataA")?.value || ""),
6167
6238
  primaCasaMutuoPerc1: num("primaCasaMutuoPerc1"),
6168
6239
  straordAnn1: num("straordAnn1"),
@@ -6290,8 +6361,10 @@ ${scenarioLab.length ? `
6290
6361
  });
6291
6362
  const firstHomeEnabled = document.getElementById("primaCasaMutuoEnabled");
6292
6363
  const firstHomeAssigned = document.getElementById("primaCasaAssegnataA");
6364
+ const firstHomeExpiry = document.getElementById("primaCasaMutuoScadenza");
6293
6365
  if (firstHomeEnabled) firstHomeEnabled.checked = !!firstHomeEnabled.defaultChecked;
6294
6366
  if (firstHomeAssigned) firstHomeAssigned.value = "";
6367
+ if (firstHomeExpiry) firstHomeExpiry.value = firstHomeExpiry.defaultValue || "";
6295
6368
  permanenceCalendarState.byMonth = {};
6296
6369
  speseConvivenzaAutoMode = true;
6297
6370
  selectedScenarioIdx = -1;
@@ -6446,10 +6519,11 @@ ${scenarioLab.length ? `
6446
6519
  const textareaId = String(addRowBtn.getAttribute("data-row-add") || "");
6447
6520
  const textarea = textareaId ? document.getElementById(textareaId) : null;
6448
6521
  if (!textarea) return;
6449
- const rows = parseExpenseDetailRows(textarea.value);
6522
+ const payload = parseExpenseDetailPayload(textarea.value);
6523
+ const rows = payload.rows;
6450
6524
  if (rows.length < EXPENSE_DETAIL_MAX_ROWS) {
6451
6525
  rows.push({ what: "", amount: "", due: "" });
6452
- textarea.value = serializeExpenseDetailRows(rows);
6526
+ textarea.value = serializeExpenseDetailPayload(rows, payload.note);
6453
6527
  syncExpenseDetailTableFromStore(textarea);
6454
6528
  refreshExpenseDetailButtonState();
6455
6529
  }
@@ -6491,6 +6565,12 @@ ${scenarioLab.length ? `
6491
6565
  const tableWrap = e.target.closest("[data-detail-table]");
6492
6566
  syncExpenseDetailStoreFromTable(tableWrap);
6493
6567
  refreshExpenseDetailButtonState();
6568
+ return;
6569
+ }
6570
+ if (e.target && e.target.matches(".spese-detail-note")) {
6571
+ const tableWrap = e.target.closest("[data-detail-table]");
6572
+ syncExpenseDetailStoreFromTable(tableWrap);
6573
+ refreshExpenseDetailButtonState();
6494
6574
  }
6495
6575
  });
6496
6576
 
@@ -6630,7 +6710,7 @@ ${scenarioLab.length ? `
6630
6710
  }
6631
6711
  updateModeUi();
6632
6712
  renderAll();
6633
- } else if (e.target && (e.target.id === "primaCasaMutuoEnabled" || e.target.id === "primaCasaAssegnataA")) {
6713
+ } else if (e.target && (e.target.id === "primaCasaMutuoEnabled" || e.target.id === "primaCasaAssegnataA" || e.target.id === "primaCasaMutuoScadenza")) {
6634
6714
  updateFirstHomeMortgageUi();
6635
6715
  renderAll();
6636
6716
  }
@@ -358,11 +358,37 @@
358
358
  </label>
359
359
  <input id="primaCasaMutuoEnabled" type="checkbox" />
360
360
  </div>
361
- <div class="field">
362
- <label for="primaCasaValoreLocativo" class="label-row"><span id="lblPrimaCasaValoreLocativo">Casa (valore locativo) ({currency})</span>
363
- <span class="hint" id="hintPrimaCasaValoreLocativo" title="Valore locativo mensile della casa assegnata, usato per valorizzare il beneficio economico implicito.">i</span>
364
- </label>
365
- <input id="primaCasaValoreLocativo" type="number" min="0" step="50" value="1000" />
361
+ <div class="field first-home-mortgage-table-field">
362
+ <div class="first-home-mortgage-table-wrap">
363
+ <table class="first-home-mortgage-table" aria-label="Dati mutuo prima casa">
364
+ <thead>
365
+ <tr>
366
+ <th>
367
+ <span class="label-row"><span id="lblPrimaCasaValoreLocativo">Casa (valore locativo) ({currency})</span>
368
+ <span class="hint" id="hintPrimaCasaValoreLocativo" title="Valore locativo mensile della casa assegnata, usato per valorizzare il beneficio economico implicito.">i</span>
369
+ </span>
370
+ </th>
371
+ <th>
372
+ <span class="label-row"><span id="lblPrimaCasaMutuoImporto">Rata mutuo mensile ({currency})</span>
373
+ <span class="hint" id="hintPrimaCasaMutuoImporto" title="Importo mensile complessivo della rata del mutuo prima casa.">i</span>
374
+ </span>
375
+ </th>
376
+ <th>
377
+ <span class="label-row"><span id="lblPrimaCasaMutuoScadenza">Scadenza mutuo</span>
378
+ <span class="hint" id="hintPrimaCasaMutuoScadenza" title="Data prevista di estinzione del mutuo prima casa.">i</span>
379
+ </span>
380
+ </th>
381
+ </tr>
382
+ </thead>
383
+ <tbody>
384
+ <tr>
385
+ <td><input id="primaCasaValoreLocativo" type="number" min="0" step="50" value="1000" /></td>
386
+ <td><input id="primaCasaMutuoImporto" type="number" min="0" step="50" value="0" /></td>
387
+ <td><input id="primaCasaMutuoScadenza" type="date" value="" /></td>
388
+ </tr>
389
+ </tbody>
390
+ </table>
391
+ </div>
366
392
  </div>
367
393
  <div class="field">
368
394
  <label for="primaCasaAssegnataA" class="label-row"><span id="lblPrimaCasaAssegnataA">Casa assegnata a</span>
@@ -374,15 +400,9 @@
374
400
  <option value="2">Coniuge 2</option>
375
401
  </select>
376
402
  </div>
377
- <div class="field">
378
- <label for="primaCasaMutuoImporto" class="label-row"><span id="lblPrimaCasaMutuoImporto">Rata mutuo mensile ({currency})</span>
379
- <span class="hint" id="hintPrimaCasaMutuoImporto" title="Importo mensile complessivo della rata del mutuo prima casa.">i</span>
380
- </label>
381
- <input id="primaCasaMutuoImporto" type="number" min="0" step="50" value="0" />
382
- </div>
383
403
  <div class="field first-home-split-field">
384
404
  <label for="primaCasaMutuoPerc1" class="label-row"><span id="lblPrimaCasaMutuoPerc1">Quota mutuo a carico</span>
385
- <span class="hint" id="hintPrimaCasaMutuoPerc1" title="Percentuale della rata mutuo pagata da Coniuge 1. La quota dell'altro coniuge e complementare a 100%.">i</span>
405
+ <span class="hint" id="hintPrimaCasaMutuoPerc1" title="Percentuale della rata mutuo pagata da ciascun Coniuge.">i</span>
386
406
  </label>
387
407
  <div class="mortgage-split-slider" id="primaCasaMutuoSliderWrap">
388
408
  <div class="mortgage-split-side mortgage-split-side-left" id="primaCasaSplitLeft">
@@ -630,7 +650,7 @@
630
650
  <script src="supabase.min.js"></script>
631
651
  <script src="fabric.min.js"></script>
632
652
  <script src="html2pdf.bundle.min.js"></script>
633
- <script src="app.js?v=2.4.9"></script>
653
+ <script src="app.js?v=2.4.11"></script>
634
654
  </body>
635
655
  </html>
636
656
 
@@ -949,6 +949,35 @@
949
949
  grid-column: 1 / -1;
950
950
  }
951
951
 
952
+ .extra-box-first-home .first-home-mortgage-table-field {
953
+ grid-column: 1 / -1;
954
+ }
955
+
956
+ .first-home-mortgage-table-wrap {
957
+ overflow-x: auto;
958
+ padding-bottom: 2px;
959
+ }
960
+
961
+ .first-home-mortgage-table {
962
+ width: 100%;
963
+ min-width: 560px;
964
+ border-collapse: separate;
965
+ border-spacing: 6px;
966
+ table-layout: fixed;
967
+ }
968
+
969
+ .first-home-mortgage-table th {
970
+ text-align: left;
971
+ font-size: 0.8rem;
972
+ color: #2a5954;
973
+ font-weight: 800;
974
+ padding: 0 2px;
975
+ }
976
+
977
+ .first-home-mortgage-table td {
978
+ padding: 0;
979
+ }
980
+
952
981
  .extra-box-first-home .field {
953
982
  border-radius: 12px;
954
983
  padding: 8px 9px 7px;
@@ -966,6 +995,7 @@
966
995
  }
967
996
 
968
997
  .extra-box-first-home .field input[type="number"],
998
+ .extra-box-first-home .field input[type="date"],
969
999
  .extra-box-first-home .field select {
970
1000
  border-width: 1.5px;
971
1001
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8) inset;
@@ -982,6 +1012,7 @@
982
1012
  }
983
1013
 
984
1014
  .extra-box-first-home .field input[type="number"]:focus,
1015
+ .extra-box-first-home .field input[type="date"]:focus,
985
1016
  .extra-box-first-home .field select:focus {
986
1017
  border-color: rgba(27, 141, 127, 0.4);
987
1018
  box-shadow: 0 0 0 3px rgba(27, 141, 127, 0.14), 0 1px 0 rgba(255, 255, 255, 0.85) inset;
@@ -1592,6 +1623,20 @@
1592
1623
  font-size: 0.67rem;
1593
1624
  }
1594
1625
 
1626
+ .spese-detail-note {
1627
+ width: 100%;
1628
+ margin-top: 6px;
1629
+ border: 1px solid #bfd5d0;
1630
+ border-radius: 6px;
1631
+ background: #ffffff;
1632
+ color: #214b47;
1633
+ font-size: 0.72rem;
1634
+ line-height: 1.3;
1635
+ padding: 6px 7px;
1636
+ resize: vertical;
1637
+ min-height: 46px;
1638
+ }
1639
+
1595
1640
  .spese-detail-counter {
1596
1641
  margin-top: 3px;
1597
1642
  font-size: 0.66rem;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mantenimento-app",
3
- "version": "2.4.9",
3
+ "version": "2.4.11",
4
4
  "description": "Frontend + backend architecture for the mantenimento calculator",
5
5
  "type": "commonjs",
6
6
  "main": "backend/calculate-model.js",