mantenimento-app 1.1.2 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -88,3 +88,39 @@ Opzioni tipiche:
88
88
  ## Note
89
89
  - Strumento orientativo: non sostituisce valutazione legale/professionale.
90
90
  - Per ulteriore hardening: rate limit API, auth server-side, logging audit, WAF/CDN.
91
+
92
+ ## Compliance GDPR e legale (checklist operativa)
93
+ Questa checklist aiuta a ridurre rischi di non conformita per istanze pubblicate in Italia/UE.
94
+
95
+ 1. Governance e ruoli
96
+ - Definisci il titolare del trattamento per ogni istanza pubblicata.
97
+ - Se usi fornitori cloud, verifica nomina a responsabile del trattamento (DPA) dove richiesto.
98
+
99
+ 2. Documentazione minima
100
+ - Mantieni aggiornate `privacy.html`, `cookie.html` e `termini.html`.
101
+ - Mantieni un registro dei trattamenti (art. 30 GDPR) se applicabile alla tua organizzazione.
102
+
103
+ 3. Basi giuridiche e minimizzazione
104
+ - Tratta solo dati necessari al calcolo e alle funzioni richieste.
105
+ - Documenta base giuridica per account, sincronizzazione e sicurezza.
106
+
107
+ 4. Sicurezza
108
+ - Non salvare segreti backend nel frontend.
109
+ - Mantieni cifratura lato client del profilo cloud.
110
+ - Applica controllo accessi, backup e monitoraggio su infrastruttura server.
111
+
112
+ 5. Conservazione e diritti interessati
113
+ - Definisci tempi/criteri di retention per dati cloud e log tecnici.
114
+ - Predisponi processo per richieste di accesso, rettifica, cancellazione e opposizione.
115
+
116
+ 6. Cookie e storage locale
117
+ - Usa storage locale solo per finalita tecniche necessarie.
118
+ - In caso di strumenti analytics/marketing futuri, valuta banner e consenso dove richiesto.
119
+
120
+ 7. Licenze software
121
+ - Verifica compatibilita licenze di dipendenze e librerie terze.
122
+ - Mantieni file `LICENSE` e attribuzioni necessarie nei materiali distribuiti.
123
+
124
+ 8. Ambito medico-legale
125
+ - Questo progetto NON e un dispositivo medico e non fornisce pareri legali.
126
+ - Evita claim diagnostici/clinici o automatismi decisionali su diritti soggettivi.
package/app.js CHANGED
@@ -183,6 +183,8 @@ const defaultExpenseItems = [
183
183
  incomeHintAnnual: "Entrate nette annuali di {spouse}: il sistema converte automaticamente in quota mensile (/12).",
184
184
  incomeHintMonthly: "Entrate nette mensili disponibili di {spouse}.",
185
185
  liveNetAvailablePerson: "Netto disponibile {spouse}",
186
+ liveNetPostSupportPerson: "Netto post-assegno {spouse}",
187
+ liveDaysWithSpouse: "{days} gg con {spouse}",
186
188
  liveTotalIncome: "Entrate totali (reddito + assegni + INPS)",
187
189
  livePaidToOther: "Assegno mantenimento pagato all'altro coniuge",
188
190
  livePaidExternal: "Assegno mantenimento pagato esterno",
@@ -379,6 +381,8 @@ const defaultExpenseItems = [
379
381
  incomeHintAnnual: "Yearly net income for {spouse}: automatically converted to monthly amount (/12).",
380
382
  incomeHintMonthly: "Available monthly net income for {spouse}.",
381
383
  liveNetAvailablePerson: "Net available {spouse}",
384
+ liveNetPostSupportPerson: "Post-support net {spouse}",
385
+ liveDaysWithSpouse: "{days} days with {spouse}",
382
386
  liveTotalIncome: "Total income (income + support + INPS)",
383
387
  livePaidToOther: "Support paid to the other spouse",
384
388
  livePaidExternal: "External support paid",
@@ -461,6 +465,7 @@ const defaultExpenseItems = [
461
465
  let incomeModeLast = "monthly";
462
466
  let currentLang = "it";
463
467
  let currentCurrency = "EUR";
468
+ const CALC_API_BASE = String(window.KEYLOCK_CALC_API_BASE || "").trim().replace(/\/+$/, "");
464
469
 
465
470
  function tr(key) {
466
471
  const table = I18N[currentLang] || I18N.it;
@@ -485,6 +490,42 @@ const defaultExpenseItems = [
485
490
  return n * rate;
486
491
  }
487
492
 
493
+ function resolveCalculationApiUrl() {
494
+ if (CALC_API_BASE) return `${CALC_API_BASE}/api/calculate`;
495
+ const host = String((window.location && window.location.hostname) || "").toLowerCase();
496
+ if (host === "localhost" || host === "127.0.0.1") return "/api/calculate";
497
+ return null;
498
+ }
499
+
500
+ function patchFabricTextBaselineTypo() {
501
+ if (!window.fabric || !window.fabric.Text || !window.fabric.Text.prototype) return;
502
+ const proto = window.fabric.Text.prototype;
503
+ if (proto.__keylockBaselinePatched) return;
504
+
505
+ // Fabric 5 in this bundle uses the non-standard baseline value "alphabetical".
506
+ proto._setTextStyles = function(ctx, styleDeclaration, forMeasuring) {
507
+ if (!ctx) return;
508
+ ctx.textBaseline = "alphabetic";
509
+ if (this.path) {
510
+ switch (this.pathAlign) {
511
+ case "center":
512
+ ctx.textBaseline = "middle";
513
+ break;
514
+ case "ascender":
515
+ ctx.textBaseline = "top";
516
+ break;
517
+ case "descender":
518
+ ctx.textBaseline = "bottom";
519
+ break;
520
+ default:
521
+ break;
522
+ }
523
+ }
524
+ ctx.font = this._getFontDeclaration(styleDeclaration, forMeasuring);
525
+ };
526
+ proto.__keylockBaselinePatched = true;
527
+ }
528
+
488
529
  function normalizeUiZoom(value) {
489
530
  const n = Number(value);
490
531
  if (!Number.isFinite(n)) return 1;
@@ -1118,21 +1159,37 @@ const defaultExpenseItems = [
1118
1159
 
1119
1160
  function initCoffeeDonationPicker() {
1120
1161
  const heroCoffeeBtn = document.querySelector(".btn-coffee-hero");
1162
+ const heroDonateWrap = document.querySelector(".hero-donate");
1163
+ const heroDonateMenu = document.querySelector(".hero-donate-menu");
1121
1164
  const coffeeFloat = document.querySelector(".coffee-float");
1122
1165
  const floatBtn = document.querySelector(".coffee-float-btn");
1123
1166
  const floatCard = document.querySelector(".coffee-float-card");
1124
1167
  const donateBanner = document.querySelector(".donate-banner");
1125
1168
  if (!heroCoffeeBtn || !coffeeFloat || !floatBtn || !floatCard) return;
1126
1169
 
1170
+ function setHeroDonateOpen(open) {
1171
+ if (!heroDonateWrap) return;
1172
+ heroDonateWrap.classList.toggle("is-open", !!open);
1173
+ heroCoffeeBtn.setAttribute("aria-expanded", open ? "true" : "false");
1174
+ }
1175
+
1127
1176
  floatBtn.setAttribute("aria-haspopup", "true");
1128
1177
  floatBtn.setAttribute("aria-expanded", "false");
1178
+ heroCoffeeBtn.setAttribute("aria-haspopup", "true");
1179
+ heroCoffeeBtn.setAttribute("aria-expanded", "false");
1129
1180
 
1130
1181
  heroCoffeeBtn.addEventListener("click", (e) => {
1131
- // Keep the href as no-JS fallback, but open the local payment chooser when JS is active.
1132
1182
  e.preventDefault();
1133
- setCoffeePickerOpen(true);
1183
+ const willOpen = !(heroDonateWrap && heroDonateWrap.classList.contains("is-open"));
1184
+ setHeroDonateOpen(willOpen);
1134
1185
  });
1135
1186
 
1187
+ if (heroDonateMenu) {
1188
+ heroDonateMenu.addEventListener("click", (e) => {
1189
+ if (e.target && e.target.closest("a")) setHeroDonateOpen(false);
1190
+ });
1191
+ }
1192
+
1136
1193
  if (donateBanner) {
1137
1194
  donateBanner.addEventListener("click", (e) => {
1138
1195
  // Let payment links keep their native behavior.
@@ -1161,12 +1218,16 @@ const defaultExpenseItems = [
1161
1218
  });
1162
1219
 
1163
1220
  document.addEventListener("click", (e) => {
1164
- if (e.target.closest(".coffee-float") || e.target.closest(".btn-coffee-hero")) return;
1221
+ if (e.target.closest(".coffee-float") || e.target.closest(".hero-donate")) return;
1222
+ setHeroDonateOpen(false);
1165
1223
  setCoffeePickerOpen(false);
1166
1224
  });
1167
1225
 
1168
1226
  document.addEventListener("keydown", (e) => {
1169
- if (e.key === "Escape") setCoffeePickerOpen(false);
1227
+ if (e.key === "Escape") {
1228
+ setHeroDonateOpen(false);
1229
+ setCoffeePickerOpen(false);
1230
+ }
1170
1231
  });
1171
1232
  }
1172
1233
 
@@ -1474,8 +1535,8 @@ const defaultExpenseItems = [
1474
1535
  expenseItems.forEach((item, idx) => {
1475
1536
  const labelEsc = escapeHtml(item.label);
1476
1537
  const helpEsc = escapeHtml(item.help);
1477
- const tr = document.createElement("tr");
1478
- tr.innerHTML = `
1538
+ const rowEl = document.createElement("tr");
1539
+ rowEl.innerHTML = `
1479
1540
  <td><span class="label-row">${labelEsc}<span class="hint" title="${helpEsc}">i</span></span></td>
1480
1541
  <td>
1481
1542
  <div class="spese-input-wrap">
@@ -1493,7 +1554,7 @@ const defaultExpenseItems = [
1493
1554
  <button class="btn-secondary spese-remove-btn" type="button" data-remove-expense-idx="${idx}" title="${tr("expenseRemoveTitle")}">${tr("expenseRemoveBtn")}</button>
1494
1555
  </td>
1495
1556
  `;
1496
- rowsSpese.appendChild(tr);
1557
+ rowsSpese.appendChild(rowEl);
1497
1558
  });
1498
1559
  }
1499
1560
 
@@ -1746,18 +1807,21 @@ const defaultExpenseItems = [
1746
1807
 
1747
1808
  function computeModel() {
1748
1809
  const payload = collectCalculationPayload();
1749
- try {
1750
- const xhr = new XMLHttpRequest();
1751
- xhr.open("POST", "/api/calculate", false);
1752
- xhr.setRequestHeader("Content-Type", "application/json");
1753
- xhr.send(JSON.stringify(payload));
1754
-
1755
- if (xhr.status >= 200 && xhr.status < 300) {
1756
- const body = JSON.parse(xhr.responseText || "{}");
1757
- if (body && body.ok && body.model) return body.model;
1810
+ const apiUrl = resolveCalculationApiUrl();
1811
+ if (apiUrl) {
1812
+ try {
1813
+ const xhr = new XMLHttpRequest();
1814
+ xhr.open("POST", apiUrl, false);
1815
+ xhr.setRequestHeader("Content-Type", "application/json");
1816
+ xhr.send(JSON.stringify(payload));
1817
+
1818
+ if (xhr.status >= 200 && xhr.status < 300) {
1819
+ const body = JSON.parse(xhr.responseText || "{}");
1820
+ if (body && body.ok && body.model) return body.model;
1821
+ }
1822
+ } catch (_) {
1823
+ // Fallback handled below.
1758
1824
  }
1759
- } catch (_) {
1760
- // Fallback handled below.
1761
1825
  }
1762
1826
 
1763
1827
  return computeModelLocal(payload);
@@ -1780,21 +1844,27 @@ const defaultExpenseItems = [
1780
1844
  function renderLivePanel(m) {
1781
1845
  const liveNet = document.getElementById("liveNet");
1782
1846
  const liveBreakdown = document.getElementById("liveBreakdown");
1847
+ if (!liveNet || !liveBreakdown) return;
1783
1848
 
1784
1849
  const entrate1 = m.r1 + m.aPerc1 + m.aFam1;
1785
1850
  const entrate2 = m.r2 + m.aPerc2 + m.aFam2;
1786
1851
  const uscite1 = m.match12 + m.esternoPag1 + m.spese1;
1787
1852
  const uscite2 = m.match21 + m.esternoPag2 + m.spese2;
1788
- const diffDisp = m.disp1 - m.disp2;
1853
+ const hasSuggestedSupport = Math.max(m.assegnoDa1a2, m.assegnoDa2a1) > 0.005;
1854
+ const shownDisp1 = hasSuggestedSupport ? m.post1 : m.disp1;
1855
+ const shownDisp2 = hasSuggestedSupport ? m.post2 : m.disp2;
1856
+ const liveNetLabelKey = hasSuggestedSupport ? "liveNetPostSupportPerson" : "liveNetAvailablePerson";
1857
+ const diffDisp = shownDisp1 - shownDisp2;
1789
1858
  const absDiffDisp = Math.abs(diffDisp);
1859
+
1790
1860
  liveNet.innerHTML = `
1791
1861
  <div class="live-k">
1792
- ${msg("liveNetAvailablePerson", { spouse: c1n() })}
1793
- <strong class="${m.disp1 >= 0 ? "ok" : "bad"}">${eur(m.disp1)}</strong>
1862
+ ${msg(liveNetLabelKey, { spouse: c1n() })}
1863
+ <strong class="${shownDisp1 >= 0 ? "ok" : "bad"}">${eur(shownDisp1)}</strong>
1794
1864
  </div>
1795
1865
  <div class="live-k">
1796
- ${msg("liveNetAvailablePerson", { spouse: c2n() })}
1797
- <strong class="${m.disp2 >= 0 ? "ok" : "bad"}">${eur(m.disp2)}</strong>
1866
+ ${msg(liveNetLabelKey, { spouse: c2n() })}
1867
+ <strong class="${shownDisp2 >= 0 ? "ok" : "bad"}">${eur(shownDisp2)}</strong>
1798
1868
  </div>
1799
1869
  <div class="live-diff">
1800
1870
  <div class="live-fabric-wrap">
@@ -1814,6 +1884,8 @@ const defaultExpenseItems = [
1814
1884
  <div class="live-row"><span>${tr("livePaidExternal")}</span><strong class="warn">${eur(m.esternoPag1)}</strong></div>
1815
1885
  <div class="live-row"><span>${tr("liveTotalExpensesEntered")}</span><strong class="warn">${eur(m.spese1)}</strong></div>
1816
1886
  <div class="live-row"><span>${tr("liveTotalOutflows")}</span><strong class="warn">${eur(uscite1)}</strong></div>
1887
+ ${hasSuggestedSupport ? `<div class="live-row"><span>${tr("pdfSuggestedSupport")}</span><strong class="warn">-${eur(m.assegnoDa1a2)}${m.assegnoDa2a1 > 0.005 ? ` / +${eur(m.assegnoDa2a1)}` : ""}</strong></div>` : ""}
1888
+ <div class="live-row"><span>${tr("pdfPostSupport")}</span><strong class="${shownDisp1 >= 0 ? "ok" : "bad"}">${eur(shownDisp1)}</strong></div>
1817
1889
  </div>
1818
1890
  <div class="live-col">
1819
1891
  <h4>${c2n()}</h4>
@@ -1822,6 +1894,8 @@ const defaultExpenseItems = [
1822
1894
  <div class="live-row"><span>${tr("livePaidExternal")}</span><strong class="warn">${eur(m.esternoPag2)}</strong></div>
1823
1895
  <div class="live-row"><span>${tr("liveTotalExpensesEntered")}</span><strong class="warn">${eur(m.spese2)}</strong></div>
1824
1896
  <div class="live-row"><span>${tr("liveTotalOutflows")}</span><strong class="warn">${eur(uscite2)}</strong></div>
1897
+ ${hasSuggestedSupport ? `<div class="live-row"><span>${tr("pdfSuggestedSupport")}</span><strong class="warn">-${eur(m.assegnoDa2a1)}${m.assegnoDa1a2 > 0.005 ? ` / +${eur(m.assegnoDa1a2)}` : ""}</strong></div>` : ""}
1898
+ <div class="live-row"><span>${tr("pdfPostSupport")}</span><strong class="${shownDisp2 >= 0 ? "ok" : "bad"}">${eur(shownDisp2)}</strong></div>
1825
1899
  </div>
1826
1900
  </div>
1827
1901
  `;
@@ -1852,6 +1926,13 @@ const defaultExpenseItems = [
1852
1926
  const shift = (diffDisp / totalAbs) * (radius * 1.5);
1853
1927
  const pointerX = Math.max(centerX - radius * 1.6, Math.min(centerX + radius * 1.6, centerX + shift));
1854
1928
 
1929
+ const grad = (x1, y1, x2, y2, stops) => new window.fabric.Gradient({
1930
+ type: "linear",
1931
+ gradientUnits: "pixels",
1932
+ coords: { x1, y1, x2, y2 },
1933
+ colorStops: stops
1934
+ });
1935
+
1855
1936
  fc.add(new window.fabric.Rect({
1856
1937
  left: 8,
1857
1938
  top: 8,
@@ -1886,10 +1967,10 @@ const defaultExpenseItems = [
1886
1967
 
1887
1968
  function addOutcomeFigure(left, top, spouseId, role) {
1888
1969
  const palette = role === "winner"
1889
- ? { bg: "#ddf2ec", stroke: "#9ccfc2", ink: "#0d625a" }
1970
+ ? { bgA: "#ecfff9", bgB: "#9bd9cb", stroke: "#8ec9bb", ink: "#0d625a" }
1890
1971
  : role === "loser"
1891
- ? { bg: "#f8ecd9", stroke: "#dfbf8f", ink: "#8a580f" }
1892
- : { bg: "#eaf2f0", stroke: "#bfd2cd", ink: "#3d5c59" };
1972
+ ? { bgA: "#fff8e8", bgB: "#f2be75", stroke: "#e3b677", ink: "#8a580f" }
1973
+ : { bgA: "#f6faf9", bgB: "#c5dbd6", stroke: "#bfd2cd", ink: "#3d5c59" };
1893
1974
 
1894
1975
  const scale = figRadius / 38;
1895
1976
 
@@ -1897,7 +1978,10 @@ const defaultExpenseItems = [
1897
1978
  left,
1898
1979
  top,
1899
1980
  radius: figRadius,
1900
- fill: palette.bg,
1981
+ fill: grad(left, top, left, top + (figRadius * 2), [
1982
+ { offset: 0, color: palette.bgA },
1983
+ { offset: 1, color: palette.bgB }
1984
+ ]),
1901
1985
  stroke: palette.stroke,
1902
1986
  strokeWidth: 1.5,
1903
1987
  shadow: "0 2px 6px rgba(0,0,0,0.10)"
@@ -1953,16 +2037,15 @@ const defaultExpenseItems = [
1953
2037
  }
1954
2038
 
1955
2039
  const reservedForFigures = (figDiameter * 2) + (figMargin * 2) + 16;
1956
- const badgeAvailableW = Math.max(120, Math.min(w - 20, w - reservedForFigures));
1957
- const badgeW = Math.min(520, badgeAvailableW);
2040
+ const badgeAvailableW = Math.max(132, Math.min(w - 20, w - reservedForFigures));
2041
+ const badgeW = Math.min(500, badgeAvailableW);
1958
2042
  const badgeX = (w - badgeW) / 2;
1959
2043
  const badgeText = `${statusText} | ${tr("pdfDelta")} ${eur(absDiffDisp)}`;
1960
- const badgeMaxFontSize = isNarrow ? 16 : (w < 560 ? 24 : 28);
1961
- const badgeMinFontSize = isNarrow ? 12 : 10;
1962
- const badgeInnerW = badgeW - 36;
1963
- const badgeBaseHeight = isNarrow ? 46 : (w < 560 ? 56 : 62);
2044
+ const badgeMaxFontSize = isNarrow ? 14 : (w < 560 ? 18 : 22);
2045
+ const badgeMinFontSize = isNarrow ? 10 : 10;
2046
+ const badgeInnerW = badgeW - 28;
2047
+ const badgeBaseHeight = isNarrow ? 42 : (w < 560 ? 48 : 54);
1964
2048
 
1965
- // Allow multiline on narrow screens: shrink font only if needed, then grow badge height if text still wraps.
1966
2049
  let badgeFontSize = badgeMaxFontSize;
1967
2050
  let badgeProbe = new window.fabric.Textbox(badgeText, {
1968
2051
  width: badgeInnerW,
@@ -1972,7 +2055,7 @@ const defaultExpenseItems = [
1972
2055
  textAlign: "center",
1973
2056
  lineHeight: 1.05
1974
2057
  });
1975
- while (badgeProbe.height > (badgeBaseHeight - 10) && badgeFontSize > badgeMinFontSize) {
2058
+ while (badgeProbe.height > (badgeBaseHeight - 8) && badgeFontSize > badgeMinFontSize) {
1976
2059
  badgeFontSize -= 1;
1977
2060
  badgeProbe = new window.fabric.Textbox(badgeText, {
1978
2061
  width: badgeInnerW,
@@ -1989,18 +2072,24 @@ const defaultExpenseItems = [
1989
2072
  const badgeHeight = Math.max(badgeBaseHeight, Math.ceil(badgeProbe.height) + 10);
1990
2073
  fc.add(new window.fabric.Rect({
1991
2074
  left: badgeX,
1992
- top: 18,
2075
+ top: 20,
1993
2076
  width: badgeW,
1994
2077
  height: badgeHeight,
1995
- rx: 20,
1996
- ry: 20,
1997
- fill: "#ffffff",
1998
- stroke: diffDisp === 0 ? "#98b8b3" : (diffDisp > 0 ? "#8ec3b9" : "#d9b47e"),
1999
- strokeWidth: 1
2078
+ rx: 18,
2079
+ ry: 18,
2080
+ fill: diffDisp === 0
2081
+ ? grad(badgeX, 20, badgeX, 20 + badgeHeight, [{ offset: 0, color: "#ffffff" }, { offset: 1, color: "#d6ebe6" }])
2082
+ : (diffDisp > 0
2083
+ ? grad(badgeX, 20, badgeX, 20 + badgeHeight, [{ offset: 0, color: "#f4fffc" }, { offset: 1, color: "#a9ddd2" }])
2084
+ : grad(badgeX, 20, badgeX, 20 + badgeHeight, [{ offset: 0, color: "#fffaf0" }, { offset: 1, color: "#f1cd93" }])
2085
+ ),
2086
+ stroke: diffDisp === 0 ? "#98b8b3" : (diffDisp > 0 ? "#7bbdb0" : "#d4a864"),
2087
+ strokeWidth: 1.4,
2088
+ shadow: "0 4px 10px rgba(0,0,0,0.12)"
2000
2089
  }));
2001
2090
  fc.add(new window.fabric.Textbox(badgeText, {
2002
- left: badgeX + badgeW / 2,
2003
- top: 18 + (badgeHeight - badgeProbe.height) / 2,
2091
+ left: badgeX + (badgeW / 2),
2092
+ top: 20 + ((badgeHeight - badgeProbe.height) / 2),
2004
2093
  width: badgeInnerW,
2005
2094
  originX: "center",
2006
2095
  textAlign: "center",
@@ -2031,7 +2120,10 @@ const defaultExpenseItems = [
2031
2120
  height: 18,
2032
2121
  rx: 9,
2033
2122
  ry: 9,
2034
- fill: "#bfe3da"
2123
+ fill: grad(trackLeft, centerY - 9, trackLeft + (trackWidth / 2), centerY + 9, [
2124
+ { offset: 0, color: "#bfe9df" },
2125
+ { offset: 1, color: "#6ec0b0" }
2126
+ ])
2035
2127
  }));
2036
2128
  fc.add(new window.fabric.Rect({
2037
2129
  left: centerX,
@@ -2040,7 +2132,10 @@ const defaultExpenseItems = [
2040
2132
  height: 18,
2041
2133
  rx: 9,
2042
2134
  ry: 9,
2043
- fill: "#efd7b0"
2135
+ fill: grad(centerX, centerY - 9, centerX + (trackWidth / 2), centerY + 9, [
2136
+ { offset: 0, color: "#f2d8ab" },
2137
+ { offset: 1, color: "#deaa59" }
2138
+ ])
2044
2139
  }));
2045
2140
 
2046
2141
  fc.add(new window.fabric.Rect({
@@ -2063,20 +2158,30 @@ const defaultExpenseItems = [
2063
2158
  left: pointerX - 12,
2064
2159
  top: centerY - 12,
2065
2160
  radius: 12,
2066
- fill: diffDisp >= 0 ? "#0b6e66" : "#c77a11",
2161
+ fill: diffDisp >= 0
2162
+ ? grad(pointerX - 12, centerY - 12, pointerX - 12, centerY + 12, [{ offset: 0, color: "#1a8f85" }, { offset: 1, color: "#0b6e66" }])
2163
+ : grad(pointerX - 12, centerY - 12, pointerX - 12, centerY + 12, [{ offset: 0, color: "#e39b35" }, { offset: 1, color: "#c77a11" }]),
2067
2164
  stroke: "#ffffff",
2068
2165
  strokeWidth: 3,
2069
2166
  shadow: "0 2px 7px rgba(0,0,0,0.24)"
2070
2167
  }));
2071
2168
 
2072
- // Side labels: full names, anchored to edges, with adaptive font size.
2073
- const sideLabelTop = centerY + 21;
2169
+ // Side labels: names plus effective days out of 30.
2170
+ const sideLabelTop = centerY + 18;
2171
+ const sideDaysTop = sideLabelTop + 15;
2074
2172
  const sideLabelMaxW = Math.max(58, trackWidth / 2 - 34);
2075
2173
  const sideBaseFont = 13;
2076
2174
  const sideFontFamily = "Candara";
2077
2175
  const sideFontWeight = "700";
2078
2176
 
2079
2177
  const leftName = c1n();
2178
+ const rightName = c2n();
2179
+ const days1 = Math.round((m.perm1 / 100) * 30 * 10) / 10;
2180
+ const days2 = Math.round((m.perm2 / 100) * 30 * 10) / 10;
2181
+ const fmtDays = (v) => Number.isInteger(v) ? String(v) : v.toFixed(1);
2182
+ const leftDaysLabel = msg("liveDaysWithSpouse", { days: fmtDays(days1), spouse: leftName });
2183
+ const rightDaysLabel = msg("liveDaysWithSpouse", { days: fmtDays(days2), spouse: rightName });
2184
+
2080
2185
  const leftProbe = new window.fabric.Text(leftName, {
2081
2186
  fontSize: sideBaseFont,
2082
2187
  fontFamily: sideFontFamily,
@@ -2086,7 +2191,6 @@ const defaultExpenseItems = [
2086
2191
  ? sideBaseFont
2087
2192
  : Math.max(10, Math.floor(sideBaseFont * sideLabelMaxW / leftProbe.width));
2088
2193
 
2089
- const rightName = c2n();
2090
2194
  const rightProbe = new window.fabric.Text(rightName, {
2091
2195
  fontSize: sideBaseFont,
2092
2196
  fontFamily: sideFontFamily,
@@ -2096,6 +2200,24 @@ const defaultExpenseItems = [
2096
2200
  ? sideBaseFont
2097
2201
  : Math.max(10, Math.floor(sideBaseFont * sideLabelMaxW / rightProbe.width));
2098
2202
 
2203
+ const leftDaysProbe = new window.fabric.Text(leftDaysLabel, {
2204
+ fontSize: 10,
2205
+ fontFamily: sideFontFamily,
2206
+ fontWeight: "700"
2207
+ });
2208
+ const leftDaysFont = leftDaysProbe.width <= sideLabelMaxW
2209
+ ? 10
2210
+ : Math.max(8, Math.floor(10 * sideLabelMaxW / leftDaysProbe.width));
2211
+
2212
+ const rightDaysProbe = new window.fabric.Text(rightDaysLabel, {
2213
+ fontSize: 10,
2214
+ fontFamily: sideFontFamily,
2215
+ fontWeight: "700"
2216
+ });
2217
+ const rightDaysFont = rightDaysProbe.width <= sideLabelMaxW
2218
+ ? 10
2219
+ : Math.max(8, Math.floor(10 * sideLabelMaxW / rightDaysProbe.width));
2220
+
2099
2221
  fc.add(new window.fabric.Text(leftName, {
2100
2222
  left: trackLeft,
2101
2223
  top: sideLabelTop,
@@ -2114,6 +2236,24 @@ const defaultExpenseItems = [
2114
2236
  fontFamily: sideFontFamily,
2115
2237
  fontWeight: sideFontWeight
2116
2238
  }));
2239
+ fc.add(new window.fabric.Text(leftDaysLabel, {
2240
+ left: trackLeft,
2241
+ top: sideDaysTop,
2242
+ originX: "left",
2243
+ fontSize: leftDaysFont,
2244
+ fill: "#2e6963",
2245
+ fontFamily: sideFontFamily,
2246
+ fontWeight: "700"
2247
+ }));
2248
+ fc.add(new window.fabric.Text(rightDaysLabel, {
2249
+ left: trackLeft + trackWidth,
2250
+ top: sideDaysTop,
2251
+ originX: "right",
2252
+ fontSize: rightDaysFont,
2253
+ fill: "#8a5b1f",
2254
+ fontFamily: sideFontFamily,
2255
+ fontWeight: "700"
2256
+ }));
2117
2257
 
2118
2258
  fc.renderAll();
2119
2259
  }
@@ -2124,9 +2264,12 @@ const defaultExpenseItems = [
2124
2264
  return tr("calcModeGenovaName");
2125
2265
  }
2126
2266
 
2127
- function calculate() {
2128
- const m = computeModel();
2267
+ function calculate(model = null) {
2268
+ const m = model || computeModel();
2129
2269
  const formulaNote = document.getElementById("formulaNote");
2270
+ const resultMain = document.getElementById("risultatoMain");
2271
+ const kpi = document.getElementById("kpi");
2272
+ if (!formulaNote || !resultMain || !kpi) return;
2130
2273
 
2131
2274
  const modeName = getModeName(m.mode, m.simplePerc);
2132
2275
 
@@ -2192,9 +2335,8 @@ const defaultExpenseItems = [
2192
2335
  } else if (m.assegnoDa2a1 > 0.005) {
2193
2336
  mainText = `${c2n()} \u2192 ${c1n()}: ${eur(m.assegnoDa2a1)} ${tr("pdfPerMonth")}`;
2194
2337
  }
2195
- document.getElementById("risultatoMain").textContent = mainText;
2338
+ resultMain.textContent = mainText;
2196
2339
 
2197
- const kpi = document.getElementById("kpi");
2198
2340
  kpi.innerHTML = "";
2199
2341
 
2200
2342
  const items = [
@@ -2223,7 +2365,7 @@ const defaultExpenseItems = [
2223
2365
  const m = computeModel();
2224
2366
  updateExpensePartials();
2225
2367
  renderLivePanel(m);
2226
- calculate();
2368
+ calculate(m);
2227
2369
  }
2228
2370
 
2229
2371
  function applyState(state) {
@@ -2955,6 +3097,7 @@ const defaultExpenseItems = [
2955
3097
 
2956
3098
  populateSuggestedExpenseOptions();
2957
3099
  initPreferences();
3100
+ patchFabricTextBaselineTypo();
2958
3101
  applyStaticTranslations();
2959
3102
  buildExpenseRows();
2960
3103
  initUiZoom();