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 +36 -0
- package/app.js +197 -54
- package/frontend/public/app.js +197 -54
- package/frontend/public/cookie.html +97 -0
- package/frontend/public/index.html +9 -2
- package/frontend/public/privacy.html +78 -23
- package/frontend/public/sitemap.xml +5 -0
- package/frontend/public/styles.css +40 -1
- package/frontend/public/termini.html +51 -14
- package/package.json +1 -1
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
|
-
|
|
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(".
|
|
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")
|
|
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
|
|
1478
|
-
|
|
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(
|
|
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
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
if (
|
|
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
|
|
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(
|
|
1793
|
-
<strong class="${
|
|
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(
|
|
1797
|
-
<strong class="${
|
|
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
|
-
? {
|
|
1970
|
+
? { bgA: "#ecfff9", bgB: "#9bd9cb", stroke: "#8ec9bb", ink: "#0d625a" }
|
|
1890
1971
|
: role === "loser"
|
|
1891
|
-
? {
|
|
1892
|
-
: {
|
|
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:
|
|
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(
|
|
1957
|
-
const badgeW = Math.min(
|
|
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 ?
|
|
1961
|
-
const badgeMinFontSize = isNarrow ?
|
|
1962
|
-
const badgeInnerW = badgeW -
|
|
1963
|
-
const badgeBaseHeight = isNarrow ?
|
|
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 -
|
|
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:
|
|
2075
|
+
top: 20,
|
|
1993
2076
|
width: badgeW,
|
|
1994
2077
|
height: badgeHeight,
|
|
1995
|
-
rx:
|
|
1996
|
-
ry:
|
|
1997
|
-
fill:
|
|
1998
|
-
|
|
1999
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
2073
|
-
const sideLabelTop = centerY +
|
|
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
|
-
|
|
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();
|