mantenimento-app 2.1.7 → 2.1.9
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 +103 -17
- package/frontend/public/app.js +103 -17
- package/frontend/public/index.html +3 -3
- package/frontend/public/styles.css +110 -8
- package/package.json +1 -1
package/app.js
CHANGED
|
@@ -26,7 +26,9 @@ const defaultExpenseItems = [
|
|
|
26
26
|
let selectedScenarioIdx = -1;
|
|
27
27
|
let scenarioTransitionTimer = null;
|
|
28
28
|
const SCENARIO_LAB_MAX = 3;
|
|
29
|
-
const
|
|
29
|
+
const SCENARIO_LAB_MAX_FREE = 3;
|
|
30
|
+
const SCENARIO_LAB_MAX_DONOR = 6;
|
|
31
|
+
const SCENARIO_LABELS = ["A", "B", "C", "D", "E", "F"];
|
|
30
32
|
const EXPENSE_DETAIL_MAX_CHARS = 560;
|
|
31
33
|
const EXPENSE_DETAIL_MAX_LINES = 10;
|
|
32
34
|
|
|
@@ -134,6 +136,11 @@ const defaultExpenseItems = [
|
|
|
134
136
|
authUserFallback: "utente",
|
|
135
137
|
authLogoutDone: "Logout eseguito.",
|
|
136
138
|
authLoginRequired: "Effettua prima il login.",
|
|
139
|
+
gateNeedsAuth: "Funzionalità riservata agli utenti registrati. Accedi o registrati per continuare.",
|
|
140
|
+
gateNeedsAuthTitle: "Accesso richiesto",
|
|
141
|
+
gateNeedsDonor: "Funzionalità Premium riservata ai donatori. Supporta il progetto per sbloccarla.",
|
|
142
|
+
gateNeedsDonorTitle: "Funzionalità Premium",
|
|
143
|
+
gateDonorBadge: "Premium",
|
|
137
144
|
authSaveFailed: "Salvataggio profilo fallito: {message}",
|
|
138
145
|
authCloudSaved: "Profilo cloud salvato. Versioni storiche: {count}.",
|
|
139
146
|
authLoadFailed: "Caricamento profilo fallito: {message}",
|
|
@@ -455,6 +462,11 @@ const defaultExpenseItems = [
|
|
|
455
462
|
authUserFallback: "user",
|
|
456
463
|
authLogoutDone: "Logout completed.",
|
|
457
464
|
authLoginRequired: "Please login first.",
|
|
465
|
+
gateNeedsAuth: "This feature is for registered users only. Log in or sign up to continue.",
|
|
466
|
+
gateNeedsAuthTitle: "Login required",
|
|
467
|
+
gateNeedsDonor: "Premium feature for donors. Support the project to unlock it.",
|
|
468
|
+
gateNeedsDonorTitle: "Premium feature",
|
|
469
|
+
gateDonorBadge: "Premium",
|
|
458
470
|
authSaveFailed: "Cloud profile save failed: {message}",
|
|
459
471
|
authCloudSaved: "Cloud profile saved. History versions: {count}.",
|
|
460
472
|
authLoadFailed: "Cloud profile load failed: {message}",
|
|
@@ -710,7 +722,8 @@ const defaultExpenseItems = [
|
|
|
710
722
|
const authSession = {
|
|
711
723
|
username: null,
|
|
712
724
|
userId: null,
|
|
713
|
-
keyBits: null
|
|
725
|
+
keyBits: null,
|
|
726
|
+
isDonor: false
|
|
714
727
|
};
|
|
715
728
|
const authUiState = {
|
|
716
729
|
mode: "login",
|
|
@@ -1642,6 +1655,7 @@ const defaultExpenseItems = [
|
|
|
1642
1655
|
authSession.username = username;
|
|
1643
1656
|
authSession.userId = user.id;
|
|
1644
1657
|
authSession.keyBits = await deriveSessionKeyBits(password, user.id);
|
|
1658
|
+
authSession.isDonor = localStorage.getItem(`m_donor_${user.id}`) === "1";
|
|
1645
1659
|
updateAuthUi();
|
|
1646
1660
|
return msg("authLoginAs", { username });
|
|
1647
1661
|
}
|
|
@@ -1766,6 +1780,50 @@ const defaultExpenseItems = [
|
|
|
1766
1780
|
el.textContent = message;
|
|
1767
1781
|
}
|
|
1768
1782
|
|
|
1783
|
+
function isLoggedIn() { return !!authSession.username; }
|
|
1784
|
+
function isDonorUser() { return !!authSession.isDonor; }
|
|
1785
|
+
|
|
1786
|
+
function getScenarioMaxForUser() {
|
|
1787
|
+
if (!isLoggedIn()) return 0;
|
|
1788
|
+
return isDonorUser() ? SCENARIO_LAB_MAX_DONOR : SCENARIO_LAB_MAX_FREE;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
function showAuthGateMessage(triggerEl) {
|
|
1792
|
+
setAuthMenuOpen(true);
|
|
1793
|
+
setAuthStatus(tr("gateNeedsAuth"), true);
|
|
1794
|
+
if (triggerEl) {
|
|
1795
|
+
triggerEl.classList.add("gate-flash");
|
|
1796
|
+
setTimeout(() => triggerEl.classList.remove("gate-flash"), 700);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
function showDonorGateMessage(triggerEl) {
|
|
1801
|
+
setAuthMenuOpen(true);
|
|
1802
|
+
setAuthStatus(tr("gateNeedsDonor"), true);
|
|
1803
|
+
if (triggerEl) {
|
|
1804
|
+
triggerEl.classList.add("gate-flash");
|
|
1805
|
+
setTimeout(() => triggerEl.classList.remove("gate-flash"), 700);
|
|
1806
|
+
}
|
|
1807
|
+
const donateBanner = document.querySelector(".donate-banner");
|
|
1808
|
+
if (donateBanner) setTimeout(() => donateBanner.scrollIntoView({ behavior: "smooth", block: "center" }), 200);
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
function applyFeatureGates() {
|
|
1812
|
+
const logged = isLoggedIn();
|
|
1813
|
+
const donor = isDonorUser();
|
|
1814
|
+
const authGatedIds = ["btnPdf", "btnExportJson", "btnImportJson", "btnSaveScenario"];
|
|
1815
|
+
authGatedIds.forEach((id) => {
|
|
1816
|
+
const btn = document.getElementById(id);
|
|
1817
|
+
if (!btn) return;
|
|
1818
|
+
btn.classList.toggle("gate-locked", !logged);
|
|
1819
|
+
btn.classList.toggle("gate-donor", logged && !donor && id === "btnSaveScenario" && false); // donor gate placeholder
|
|
1820
|
+
});
|
|
1821
|
+
const authMenuBtn = document.getElementById("btnAuthMenu");
|
|
1822
|
+
if (authMenuBtn) {
|
|
1823
|
+
authMenuBtn.classList.toggle("has-donor-badge", logged && donor);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1769
1827
|
function updateAuthModeUi() {
|
|
1770
1828
|
const isSignup = authUiState.mode === "signup";
|
|
1771
1829
|
const loginModeBtn = document.getElementById("btnAuthModeLogin");
|
|
@@ -1809,7 +1867,8 @@ const defaultExpenseItems = [
|
|
|
1809
1867
|
if (sessionActions) sessionActions.classList.toggle("is-hidden", !logged);
|
|
1810
1868
|
if (toggleBtn) {
|
|
1811
1869
|
toggleBtn.classList.toggle("logged", logged);
|
|
1812
|
-
|
|
1870
|
+
const badge = logged && authSession.isDonor ? ` ✦` : "";
|
|
1871
|
+
toggleBtn.querySelector("span").textContent = logged ? `${tr("authUserPrefix")}: ${authSession.username}${badge}` : tr("authLogin");
|
|
1813
1872
|
}
|
|
1814
1873
|
|
|
1815
1874
|
if (logged) {
|
|
@@ -1833,6 +1892,7 @@ const defaultExpenseItems = [
|
|
|
1833
1892
|
setAuthStatus(tr("authNotAuthenticated"), false);
|
|
1834
1893
|
}
|
|
1835
1894
|
|
|
1895
|
+
applyFeatureGates();
|
|
1836
1896
|
void syncPresenceTrackState();
|
|
1837
1897
|
}
|
|
1838
1898
|
|
|
@@ -2377,6 +2437,7 @@ const defaultExpenseItems = [
|
|
|
2377
2437
|
authSession.username = null;
|
|
2378
2438
|
authSession.userId = null;
|
|
2379
2439
|
authSession.keyBits = null;
|
|
2440
|
+
authSession.isDonor = false;
|
|
2380
2441
|
cloudProfileSession.loaded = null;
|
|
2381
2442
|
cloudProfileSession.history = [];
|
|
2382
2443
|
updateAuthUi();
|
|
@@ -3930,7 +3991,7 @@ const defaultExpenseItems = [
|
|
|
3930
3991
|
}
|
|
3931
3992
|
|
|
3932
3993
|
function saveCurrentScenario() {
|
|
3933
|
-
if (scenarioLab.length >=
|
|
3994
|
+
if (scenarioLab.length >= getScenarioMaxForUser()) {
|
|
3934
3995
|
alert(tr("scenarioLabMaxReached"));
|
|
3935
3996
|
return;
|
|
3936
3997
|
}
|
|
@@ -4376,33 +4437,54 @@ const defaultExpenseItems = [
|
|
|
4376
4437
|
${m.incomeMode === "cu" ? `<br /><strong>${tr("calcIncomeBaseNote")}</strong> ${tr("cuNetNoteText")}` : ""}
|
|
4377
4438
|
`;
|
|
4378
4439
|
|
|
4379
|
-
let mainHtml
|
|
4440
|
+
let mainHtml;
|
|
4380
4441
|
if (m.assegnoDa1a2 > 0.005) {
|
|
4442
|
+
const perChild = m.figli > 1 ? `<div class="result-transfer-child">${eur(m.assegnoDa1a2 / m.figli)} ${escapeHtml(currentLang === "en" ? "per child" : "per figlio")}</div>` : "";
|
|
4381
4443
|
mainHtml = `
|
|
4382
|
-
<
|
|
4383
|
-
|
|
4444
|
+
<div class="result-transfer-dir">
|
|
4445
|
+
<span class="result-chip-name">${escapeHtml(c1n())}</span>
|
|
4446
|
+
<span class="result-chip-arr">→</span>
|
|
4447
|
+
<span class="result-chip-name">${escapeHtml(c2n())}</span>
|
|
4448
|
+
</div>
|
|
4449
|
+
<div class="result-transfer-value">${eur(m.assegnoDa1a2)}<span class="result-transfer-per"> ${escapeHtml(tr("pdfPerMonth"))}</span></div>
|
|
4450
|
+
${perChild}
|
|
4384
4451
|
`;
|
|
4385
4452
|
} else if (m.assegnoDa2a1 > 0.005) {
|
|
4453
|
+
const perChild = m.figli > 1 ? `<div class="result-transfer-child">${eur(m.assegnoDa2a1 / m.figli)} ${escapeHtml(currentLang === "en" ? "per child" : "per figlio")}</div>` : "";
|
|
4386
4454
|
mainHtml = `
|
|
4387
|
-
<
|
|
4388
|
-
|
|
4455
|
+
<div class="result-transfer-dir">
|
|
4456
|
+
<span class="result-chip-name">${escapeHtml(c2n())}</span>
|
|
4457
|
+
<span class="result-chip-arr">→</span>
|
|
4458
|
+
<span class="result-chip-name">${escapeHtml(c1n())}</span>
|
|
4459
|
+
</div>
|
|
4460
|
+
<div class="result-transfer-value">${eur(m.assegnoDa2a1)}<span class="result-transfer-per"> ${escapeHtml(tr("pdfPerMonth"))}</span></div>
|
|
4461
|
+
${perChild}
|
|
4389
4462
|
`;
|
|
4390
4463
|
} else {
|
|
4391
4464
|
const benefitRows = getCompensativeBenefitRows(m, c1n(), c2n());
|
|
4465
|
+
let benefitCardsHtml = "";
|
|
4392
4466
|
if (benefitRows.length) {
|
|
4393
|
-
const
|
|
4394
|
-
.
|
|
4467
|
+
const rawBenefs = Array.isArray(m.compensativeBenefits)
|
|
4468
|
+
? m.compensativeBenefits.filter((r) => r && Number(r.amount || 0) > 0.005)
|
|
4469
|
+
: [];
|
|
4470
|
+
const typeIcons = { family: "\uD83C\uDFDB", "primary-home-mortgage": "\uD83C\uDFE1" };
|
|
4471
|
+
const cardsHtml = benefitRows
|
|
4472
|
+
.map((row, i) => {
|
|
4473
|
+
const icon = (rawBenefs[i] && typeIcons[rawBenefs[i].type]) || "\u2726";
|
|
4474
|
+
return `<li class="spieg-benefit-card"><span class="spieg-benefit-icon">${icon}</span><span class="spieg-benefit-label">${escapeHtml(row.label)}</span><strong class="spieg-benefit-amount">${eur(row.amount)}</strong></li>`;
|
|
4475
|
+
})
|
|
4395
4476
|
.join("");
|
|
4396
|
-
|
|
4397
|
-
|
|
4477
|
+
const total = benefitRows.reduce((s, r) => s + r.amount, 0);
|
|
4478
|
+
const totalLabel = currentLang === "en" ? "Total allocated benefits" : "Totale benefici allocati";
|
|
4479
|
+
benefitCardsHtml = `
|
|
4398
4480
|
<div class="result-benefits-box">
|
|
4399
|
-
<
|
|
4400
|
-
<ul class="
|
|
4481
|
+
<div class="spieg-benefits-label">🎁 ${escapeHtml(tr("calcCompBenefitsLabel"))}</div>
|
|
4482
|
+
<ul class="spieg-benefits-cards">${cardsHtml}</ul>
|
|
4483
|
+
<div class="spieg-benefits-total"><span>${escapeHtml(totalLabel)}</span><strong>${eur(total)}</strong></div>
|
|
4401
4484
|
</div>
|
|
4402
4485
|
`;
|
|
4403
|
-
} else if (benefitsInline) {
|
|
4404
|
-
mainHtml = `<span class="result-main-line">${escapeHtml(msg("calcNoTransferWithBenefits", { benefits: benefitsInline }))}</span>`;
|
|
4405
4486
|
}
|
|
4487
|
+
mainHtml = `<div class="spieg-no-transfer-badge">⚖️ ${escapeHtml(tr("calcNoTransferSuggested"))}</div>${benefitCardsHtml}`;
|
|
4406
4488
|
}
|
|
4407
4489
|
resultMain.innerHTML = mainHtml;
|
|
4408
4490
|
|
|
@@ -5417,10 +5499,12 @@ ${scenarioLab.length ? `
|
|
|
5417
5499
|
document.getElementById("btnReset").addEventListener("click", resetAll);
|
|
5418
5500
|
|
|
5419
5501
|
document.getElementById("btnExportJson").addEventListener("click", async () => {
|
|
5502
|
+
if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnExportJson")); return; }
|
|
5420
5503
|
await exportJson();
|
|
5421
5504
|
});
|
|
5422
5505
|
|
|
5423
5506
|
document.getElementById("btnImportJson").addEventListener("click", () => {
|
|
5507
|
+
if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnImportJson")); return; }
|
|
5424
5508
|
document.getElementById("fileJson").click();
|
|
5425
5509
|
});
|
|
5426
5510
|
|
|
@@ -5433,10 +5517,12 @@ ${scenarioLab.length ? `
|
|
|
5433
5517
|
});
|
|
5434
5518
|
|
|
5435
5519
|
document.getElementById("btnPdf").addEventListener("click", () => {
|
|
5520
|
+
if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnPdf")); return; }
|
|
5436
5521
|
exportPdfDirect();
|
|
5437
5522
|
});
|
|
5438
5523
|
|
|
5439
5524
|
document.getElementById("btnSaveScenario").addEventListener("click", () => {
|
|
5525
|
+
if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnSaveScenario")); return; }
|
|
5440
5526
|
saveCurrentScenario();
|
|
5441
5527
|
});
|
|
5442
5528
|
|
package/frontend/public/app.js
CHANGED
|
@@ -26,7 +26,9 @@ const defaultExpenseItems = [
|
|
|
26
26
|
let selectedScenarioIdx = -1;
|
|
27
27
|
let scenarioTransitionTimer = null;
|
|
28
28
|
const SCENARIO_LAB_MAX = 3;
|
|
29
|
-
const
|
|
29
|
+
const SCENARIO_LAB_MAX_FREE = 3;
|
|
30
|
+
const SCENARIO_LAB_MAX_DONOR = 6;
|
|
31
|
+
const SCENARIO_LABELS = ["A", "B", "C", "D", "E", "F"];
|
|
30
32
|
const EXPENSE_DETAIL_MAX_CHARS = 560;
|
|
31
33
|
const EXPENSE_DETAIL_MAX_LINES = 10;
|
|
32
34
|
|
|
@@ -134,6 +136,11 @@ const defaultExpenseItems = [
|
|
|
134
136
|
authUserFallback: "utente",
|
|
135
137
|
authLogoutDone: "Logout eseguito.",
|
|
136
138
|
authLoginRequired: "Effettua prima il login.",
|
|
139
|
+
gateNeedsAuth: "Funzionalità riservata agli utenti registrati. Accedi o registrati per continuare.",
|
|
140
|
+
gateNeedsAuthTitle: "Accesso richiesto",
|
|
141
|
+
gateNeedsDonor: "Funzionalità Premium riservata ai donatori. Supporta il progetto per sbloccarla.",
|
|
142
|
+
gateNeedsDonorTitle: "Funzionalità Premium",
|
|
143
|
+
gateDonorBadge: "Premium",
|
|
137
144
|
authSaveFailed: "Salvataggio profilo fallito: {message}",
|
|
138
145
|
authCloudSaved: "Profilo cloud salvato. Versioni storiche: {count}.",
|
|
139
146
|
authLoadFailed: "Caricamento profilo fallito: {message}",
|
|
@@ -455,6 +462,11 @@ const defaultExpenseItems = [
|
|
|
455
462
|
authUserFallback: "user",
|
|
456
463
|
authLogoutDone: "Logout completed.",
|
|
457
464
|
authLoginRequired: "Please login first.",
|
|
465
|
+
gateNeedsAuth: "This feature is for registered users only. Log in or sign up to continue.",
|
|
466
|
+
gateNeedsAuthTitle: "Login required",
|
|
467
|
+
gateNeedsDonor: "Premium feature for donors. Support the project to unlock it.",
|
|
468
|
+
gateNeedsDonorTitle: "Premium feature",
|
|
469
|
+
gateDonorBadge: "Premium",
|
|
458
470
|
authSaveFailed: "Cloud profile save failed: {message}",
|
|
459
471
|
authCloudSaved: "Cloud profile saved. History versions: {count}.",
|
|
460
472
|
authLoadFailed: "Cloud profile load failed: {message}",
|
|
@@ -710,7 +722,8 @@ const defaultExpenseItems = [
|
|
|
710
722
|
const authSession = {
|
|
711
723
|
username: null,
|
|
712
724
|
userId: null,
|
|
713
|
-
keyBits: null
|
|
725
|
+
keyBits: null,
|
|
726
|
+
isDonor: false
|
|
714
727
|
};
|
|
715
728
|
const authUiState = {
|
|
716
729
|
mode: "login",
|
|
@@ -1642,6 +1655,7 @@ const defaultExpenseItems = [
|
|
|
1642
1655
|
authSession.username = username;
|
|
1643
1656
|
authSession.userId = user.id;
|
|
1644
1657
|
authSession.keyBits = await deriveSessionKeyBits(password, user.id);
|
|
1658
|
+
authSession.isDonor = localStorage.getItem(`m_donor_${user.id}`) === "1";
|
|
1645
1659
|
updateAuthUi();
|
|
1646
1660
|
return msg("authLoginAs", { username });
|
|
1647
1661
|
}
|
|
@@ -1766,6 +1780,50 @@ const defaultExpenseItems = [
|
|
|
1766
1780
|
el.textContent = message;
|
|
1767
1781
|
}
|
|
1768
1782
|
|
|
1783
|
+
function isLoggedIn() { return !!authSession.username; }
|
|
1784
|
+
function isDonorUser() { return !!authSession.isDonor; }
|
|
1785
|
+
|
|
1786
|
+
function getScenarioMaxForUser() {
|
|
1787
|
+
if (!isLoggedIn()) return 0;
|
|
1788
|
+
return isDonorUser() ? SCENARIO_LAB_MAX_DONOR : SCENARIO_LAB_MAX_FREE;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
function showAuthGateMessage(triggerEl) {
|
|
1792
|
+
setAuthMenuOpen(true);
|
|
1793
|
+
setAuthStatus(tr("gateNeedsAuth"), true);
|
|
1794
|
+
if (triggerEl) {
|
|
1795
|
+
triggerEl.classList.add("gate-flash");
|
|
1796
|
+
setTimeout(() => triggerEl.classList.remove("gate-flash"), 700);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
function showDonorGateMessage(triggerEl) {
|
|
1801
|
+
setAuthMenuOpen(true);
|
|
1802
|
+
setAuthStatus(tr("gateNeedsDonor"), true);
|
|
1803
|
+
if (triggerEl) {
|
|
1804
|
+
triggerEl.classList.add("gate-flash");
|
|
1805
|
+
setTimeout(() => triggerEl.classList.remove("gate-flash"), 700);
|
|
1806
|
+
}
|
|
1807
|
+
const donateBanner = document.querySelector(".donate-banner");
|
|
1808
|
+
if (donateBanner) setTimeout(() => donateBanner.scrollIntoView({ behavior: "smooth", block: "center" }), 200);
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
function applyFeatureGates() {
|
|
1812
|
+
const logged = isLoggedIn();
|
|
1813
|
+
const donor = isDonorUser();
|
|
1814
|
+
const authGatedIds = ["btnPdf", "btnExportJson", "btnImportJson", "btnSaveScenario"];
|
|
1815
|
+
authGatedIds.forEach((id) => {
|
|
1816
|
+
const btn = document.getElementById(id);
|
|
1817
|
+
if (!btn) return;
|
|
1818
|
+
btn.classList.toggle("gate-locked", !logged);
|
|
1819
|
+
btn.classList.toggle("gate-donor", logged && !donor && id === "btnSaveScenario" && false); // donor gate placeholder
|
|
1820
|
+
});
|
|
1821
|
+
const authMenuBtn = document.getElementById("btnAuthMenu");
|
|
1822
|
+
if (authMenuBtn) {
|
|
1823
|
+
authMenuBtn.classList.toggle("has-donor-badge", logged && donor);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1769
1827
|
function updateAuthModeUi() {
|
|
1770
1828
|
const isSignup = authUiState.mode === "signup";
|
|
1771
1829
|
const loginModeBtn = document.getElementById("btnAuthModeLogin");
|
|
@@ -1809,7 +1867,8 @@ const defaultExpenseItems = [
|
|
|
1809
1867
|
if (sessionActions) sessionActions.classList.toggle("is-hidden", !logged);
|
|
1810
1868
|
if (toggleBtn) {
|
|
1811
1869
|
toggleBtn.classList.toggle("logged", logged);
|
|
1812
|
-
|
|
1870
|
+
const badge = logged && authSession.isDonor ? ` ✦` : "";
|
|
1871
|
+
toggleBtn.querySelector("span").textContent = logged ? `${tr("authUserPrefix")}: ${authSession.username}${badge}` : tr("authLogin");
|
|
1813
1872
|
}
|
|
1814
1873
|
|
|
1815
1874
|
if (logged) {
|
|
@@ -1833,6 +1892,7 @@ const defaultExpenseItems = [
|
|
|
1833
1892
|
setAuthStatus(tr("authNotAuthenticated"), false);
|
|
1834
1893
|
}
|
|
1835
1894
|
|
|
1895
|
+
applyFeatureGates();
|
|
1836
1896
|
void syncPresenceTrackState();
|
|
1837
1897
|
}
|
|
1838
1898
|
|
|
@@ -2377,6 +2437,7 @@ const defaultExpenseItems = [
|
|
|
2377
2437
|
authSession.username = null;
|
|
2378
2438
|
authSession.userId = null;
|
|
2379
2439
|
authSession.keyBits = null;
|
|
2440
|
+
authSession.isDonor = false;
|
|
2380
2441
|
cloudProfileSession.loaded = null;
|
|
2381
2442
|
cloudProfileSession.history = [];
|
|
2382
2443
|
updateAuthUi();
|
|
@@ -3930,7 +3991,7 @@ const defaultExpenseItems = [
|
|
|
3930
3991
|
}
|
|
3931
3992
|
|
|
3932
3993
|
function saveCurrentScenario() {
|
|
3933
|
-
if (scenarioLab.length >=
|
|
3994
|
+
if (scenarioLab.length >= getScenarioMaxForUser()) {
|
|
3934
3995
|
alert(tr("scenarioLabMaxReached"));
|
|
3935
3996
|
return;
|
|
3936
3997
|
}
|
|
@@ -4376,33 +4437,54 @@ const defaultExpenseItems = [
|
|
|
4376
4437
|
${m.incomeMode === "cu" ? `<br /><strong>${tr("calcIncomeBaseNote")}</strong> ${tr("cuNetNoteText")}` : ""}
|
|
4377
4438
|
`;
|
|
4378
4439
|
|
|
4379
|
-
let mainHtml
|
|
4440
|
+
let mainHtml;
|
|
4380
4441
|
if (m.assegnoDa1a2 > 0.005) {
|
|
4442
|
+
const perChild = m.figli > 1 ? `<div class="result-transfer-child">${eur(m.assegnoDa1a2 / m.figli)} ${escapeHtml(currentLang === "en" ? "per child" : "per figlio")}</div>` : "";
|
|
4381
4443
|
mainHtml = `
|
|
4382
|
-
<
|
|
4383
|
-
|
|
4444
|
+
<div class="result-transfer-dir">
|
|
4445
|
+
<span class="result-chip-name">${escapeHtml(c1n())}</span>
|
|
4446
|
+
<span class="result-chip-arr">→</span>
|
|
4447
|
+
<span class="result-chip-name">${escapeHtml(c2n())}</span>
|
|
4448
|
+
</div>
|
|
4449
|
+
<div class="result-transfer-value">${eur(m.assegnoDa1a2)}<span class="result-transfer-per"> ${escapeHtml(tr("pdfPerMonth"))}</span></div>
|
|
4450
|
+
${perChild}
|
|
4384
4451
|
`;
|
|
4385
4452
|
} else if (m.assegnoDa2a1 > 0.005) {
|
|
4453
|
+
const perChild = m.figli > 1 ? `<div class="result-transfer-child">${eur(m.assegnoDa2a1 / m.figli)} ${escapeHtml(currentLang === "en" ? "per child" : "per figlio")}</div>` : "";
|
|
4386
4454
|
mainHtml = `
|
|
4387
|
-
<
|
|
4388
|
-
|
|
4455
|
+
<div class="result-transfer-dir">
|
|
4456
|
+
<span class="result-chip-name">${escapeHtml(c2n())}</span>
|
|
4457
|
+
<span class="result-chip-arr">→</span>
|
|
4458
|
+
<span class="result-chip-name">${escapeHtml(c1n())}</span>
|
|
4459
|
+
</div>
|
|
4460
|
+
<div class="result-transfer-value">${eur(m.assegnoDa2a1)}<span class="result-transfer-per"> ${escapeHtml(tr("pdfPerMonth"))}</span></div>
|
|
4461
|
+
${perChild}
|
|
4389
4462
|
`;
|
|
4390
4463
|
} else {
|
|
4391
4464
|
const benefitRows = getCompensativeBenefitRows(m, c1n(), c2n());
|
|
4465
|
+
let benefitCardsHtml = "";
|
|
4392
4466
|
if (benefitRows.length) {
|
|
4393
|
-
const
|
|
4394
|
-
.
|
|
4467
|
+
const rawBenefs = Array.isArray(m.compensativeBenefits)
|
|
4468
|
+
? m.compensativeBenefits.filter((r) => r && Number(r.amount || 0) > 0.005)
|
|
4469
|
+
: [];
|
|
4470
|
+
const typeIcons = { family: "\uD83C\uDFDB", "primary-home-mortgage": "\uD83C\uDFE1" };
|
|
4471
|
+
const cardsHtml = benefitRows
|
|
4472
|
+
.map((row, i) => {
|
|
4473
|
+
const icon = (rawBenefs[i] && typeIcons[rawBenefs[i].type]) || "\u2726";
|
|
4474
|
+
return `<li class="spieg-benefit-card"><span class="spieg-benefit-icon">${icon}</span><span class="spieg-benefit-label">${escapeHtml(row.label)}</span><strong class="spieg-benefit-amount">${eur(row.amount)}</strong></li>`;
|
|
4475
|
+
})
|
|
4395
4476
|
.join("");
|
|
4396
|
-
|
|
4397
|
-
|
|
4477
|
+
const total = benefitRows.reduce((s, r) => s + r.amount, 0);
|
|
4478
|
+
const totalLabel = currentLang === "en" ? "Total allocated benefits" : "Totale benefici allocati";
|
|
4479
|
+
benefitCardsHtml = `
|
|
4398
4480
|
<div class="result-benefits-box">
|
|
4399
|
-
<
|
|
4400
|
-
<ul class="
|
|
4481
|
+
<div class="spieg-benefits-label">🎁 ${escapeHtml(tr("calcCompBenefitsLabel"))}</div>
|
|
4482
|
+
<ul class="spieg-benefits-cards">${cardsHtml}</ul>
|
|
4483
|
+
<div class="spieg-benefits-total"><span>${escapeHtml(totalLabel)}</span><strong>${eur(total)}</strong></div>
|
|
4401
4484
|
</div>
|
|
4402
4485
|
`;
|
|
4403
|
-
} else if (benefitsInline) {
|
|
4404
|
-
mainHtml = `<span class="result-main-line">${escapeHtml(msg("calcNoTransferWithBenefits", { benefits: benefitsInline }))}</span>`;
|
|
4405
4486
|
}
|
|
4487
|
+
mainHtml = `<div class="spieg-no-transfer-badge">⚖️ ${escapeHtml(tr("calcNoTransferSuggested"))}</div>${benefitCardsHtml}`;
|
|
4406
4488
|
}
|
|
4407
4489
|
resultMain.innerHTML = mainHtml;
|
|
4408
4490
|
|
|
@@ -5417,10 +5499,12 @@ ${scenarioLab.length ? `
|
|
|
5417
5499
|
document.getElementById("btnReset").addEventListener("click", resetAll);
|
|
5418
5500
|
|
|
5419
5501
|
document.getElementById("btnExportJson").addEventListener("click", async () => {
|
|
5502
|
+
if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnExportJson")); return; }
|
|
5420
5503
|
await exportJson();
|
|
5421
5504
|
});
|
|
5422
5505
|
|
|
5423
5506
|
document.getElementById("btnImportJson").addEventListener("click", () => {
|
|
5507
|
+
if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnImportJson")); return; }
|
|
5424
5508
|
document.getElementById("fileJson").click();
|
|
5425
5509
|
});
|
|
5426
5510
|
|
|
@@ -5433,10 +5517,12 @@ ${scenarioLab.length ? `
|
|
|
5433
5517
|
});
|
|
5434
5518
|
|
|
5435
5519
|
document.getElementById("btnPdf").addEventListener("click", () => {
|
|
5520
|
+
if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnPdf")); return; }
|
|
5436
5521
|
exportPdfDirect();
|
|
5437
5522
|
});
|
|
5438
5523
|
|
|
5439
5524
|
document.getElementById("btnSaveScenario").addEventListener("click", () => {
|
|
5525
|
+
if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnSaveScenario")); return; }
|
|
5440
5526
|
saveCurrentScenario();
|
|
5441
5527
|
});
|
|
5442
5528
|
|
|
@@ -475,8 +475,8 @@
|
|
|
475
475
|
<div class="live-title">⚡ Impatto in Tempo Reale sul Netto</div>
|
|
476
476
|
<div class="live-grid" id="liveNet"></div>
|
|
477
477
|
<div class="result-pill">
|
|
478
|
-
<div
|
|
479
|
-
<div id="risultatoMain"
|
|
478
|
+
<div class="result-pill-head"><span>🎯</span><span>Assegno suggerito</span></div>
|
|
479
|
+
<div id="risultatoMain">Calcolo in tempo reale</div>
|
|
480
480
|
</div>
|
|
481
481
|
<div id="liveBreakdown"></div>
|
|
482
482
|
</div>
|
|
@@ -583,7 +583,7 @@
|
|
|
583
583
|
<script src="supabase.min.js"></script>
|
|
584
584
|
<script src="fabric.min.js"></script>
|
|
585
585
|
<script src="html2pdf.bundle.min.js"></script>
|
|
586
|
-
<script src="app.js?v=2.1.
|
|
586
|
+
<script src="app.js?v=2.1.9"></script>
|
|
587
587
|
</body>
|
|
588
588
|
</html>
|
|
589
589
|
|
|
@@ -1388,6 +1388,37 @@
|
|
|
1388
1388
|
border: 1px solid #c6cdc3;
|
|
1389
1389
|
}
|
|
1390
1390
|
|
|
1391
|
+
/* ── Feature gate ─────────────────────────────────────────────── */
|
|
1392
|
+
.gate-locked {
|
|
1393
|
+
position: relative;
|
|
1394
|
+
opacity: 0.62;
|
|
1395
|
+
cursor: pointer;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
.gate-locked::after {
|
|
1399
|
+
content: "\uD83D\uDD12";
|
|
1400
|
+
position: absolute;
|
|
1401
|
+
top: 2px;
|
|
1402
|
+
right: 4px;
|
|
1403
|
+
font-size: 0.65rem;
|
|
1404
|
+
line-height: 1;
|
|
1405
|
+
pointer-events: none;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
.gate-locked:hover {
|
|
1409
|
+
opacity: 0.85;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
@keyframes gate-flash-anim {
|
|
1413
|
+
0% { outline: 2px solid transparent; }
|
|
1414
|
+
30% { outline: 2px solid #d04a2f; box-shadow: 0 0 0 3px rgba(208,74,47,0.22); }
|
|
1415
|
+
100% { outline: 2px solid transparent; }
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
.gate-flash {
|
|
1419
|
+
animation: gate-flash-anim 0.65s ease forwards;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1391
1422
|
/* ── Scenario Lab ─────────────────────────────────────────────── */
|
|
1392
1423
|
.scenario-lab-card {
|
|
1393
1424
|
margin-top: 14px;
|
|
@@ -1725,8 +1756,8 @@
|
|
|
1725
1756
|
|
|
1726
1757
|
.spieg-benefit-card {
|
|
1727
1758
|
display: grid;
|
|
1728
|
-
grid-template-columns: 28px 1fr auto;
|
|
1729
|
-
align-items:
|
|
1759
|
+
grid-template-columns: 28px minmax(0, 1fr) auto;
|
|
1760
|
+
align-items: start;
|
|
1730
1761
|
gap: 8px;
|
|
1731
1762
|
padding: 8px 10px;
|
|
1732
1763
|
border-radius: 10px;
|
|
@@ -1754,6 +1785,8 @@
|
|
|
1754
1785
|
font-weight: 600;
|
|
1755
1786
|
color: #1f4642;
|
|
1756
1787
|
word-break: break-word;
|
|
1788
|
+
min-width: 0;
|
|
1789
|
+
padding-top: 5px;
|
|
1757
1790
|
}
|
|
1758
1791
|
|
|
1759
1792
|
.spieg-benefit-amount {
|
|
@@ -2209,7 +2242,6 @@
|
|
|
2209
2242
|
box-shadow: 0 12px 26px rgba(17, 70, 64, 0.16);
|
|
2210
2243
|
margin-bottom: 10px;
|
|
2211
2244
|
position: relative;
|
|
2212
|
-
overflow: hidden;
|
|
2213
2245
|
}
|
|
2214
2246
|
|
|
2215
2247
|
.result-pill::after {
|
|
@@ -2232,6 +2264,79 @@
|
|
|
2232
2264
|
color: #123b38;
|
|
2233
2265
|
}
|
|
2234
2266
|
|
|
2267
|
+
.result-pill-head {
|
|
2268
|
+
display: flex;
|
|
2269
|
+
align-items: center;
|
|
2270
|
+
gap: 7px;
|
|
2271
|
+
font-size: 0.71rem;
|
|
2272
|
+
font-weight: 800;
|
|
2273
|
+
text-transform: uppercase;
|
|
2274
|
+
letter-spacing: 0.07em;
|
|
2275
|
+
color: #3a6560;
|
|
2276
|
+
padding-bottom: 9px;
|
|
2277
|
+
margin-bottom: 9px;
|
|
2278
|
+
border-bottom: 1px solid rgba(0,0,0,0.08);
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
.result-transfer-dir {
|
|
2282
|
+
display: inline-flex;
|
|
2283
|
+
align-items: center;
|
|
2284
|
+
gap: 8px;
|
|
2285
|
+
background: linear-gradient(135deg, rgba(27, 110, 100, 0.11), rgba(27, 110, 100, 0.05));
|
|
2286
|
+
border: 1.5px solid rgba(27, 110, 100, 0.24);
|
|
2287
|
+
border-radius: 999px;
|
|
2288
|
+
padding: 6px 16px;
|
|
2289
|
+
margin-bottom: 10px;
|
|
2290
|
+
max-width: 100%;
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
.result-chip-name {
|
|
2294
|
+
font-weight: 800;
|
|
2295
|
+
font-size: 0.9rem;
|
|
2296
|
+
color: #124741;
|
|
2297
|
+
overflow-wrap: anywhere;
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
.result-chip-arr {
|
|
2301
|
+
font-size: 1.1rem;
|
|
2302
|
+
color: #1d7d72;
|
|
2303
|
+
font-weight: 900;
|
|
2304
|
+
flex-shrink: 0;
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
.result-transfer-value {
|
|
2308
|
+
display: block;
|
|
2309
|
+
font-size: 2.0rem;
|
|
2310
|
+
font-weight: 900;
|
|
2311
|
+
color: #0b5e57;
|
|
2312
|
+
line-height: 1.15;
|
|
2313
|
+
letter-spacing: -0.3px;
|
|
2314
|
+
text-shadow: 0 1px 0 rgba(255,255,255,0.75);
|
|
2315
|
+
font-variant-numeric: tabular-nums;
|
|
2316
|
+
margin-bottom: 4px;
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
.result-transfer-per {
|
|
2320
|
+
font-size: 0.48em;
|
|
2321
|
+
font-weight: 700;
|
|
2322
|
+
color: #2d7068;
|
|
2323
|
+
vertical-align: middle;
|
|
2324
|
+
margin-left: 2px;
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
.result-transfer-child {
|
|
2328
|
+
display: inline-block;
|
|
2329
|
+
margin-top: 2px;
|
|
2330
|
+
font-size: 0.78rem;
|
|
2331
|
+
color: #3d6b63;
|
|
2332
|
+
font-weight: 600;
|
|
2333
|
+
font-variant-numeric: tabular-nums;
|
|
2334
|
+
background: rgba(0,0,0,0.045);
|
|
2335
|
+
border-radius: 8px;
|
|
2336
|
+
padding: 3px 10px;
|
|
2337
|
+
border: 1px solid rgba(0,0,0,0.08);
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2235
2340
|
.result-main-flow {
|
|
2236
2341
|
display: block;
|
|
2237
2342
|
font-size: 1.02rem;
|
|
@@ -2275,12 +2380,9 @@
|
|
|
2275
2380
|
}
|
|
2276
2381
|
|
|
2277
2382
|
.result-benefits-box {
|
|
2278
|
-
border: 1px solid #bdd6cf;
|
|
2279
|
-
border-radius: 12px;
|
|
2280
|
-
background: linear-gradient(180deg, rgba(249, 253, 252, 0.92), rgba(241, 249, 246, 0.92));
|
|
2281
|
-
padding: 8px;
|
|
2282
2383
|
display: grid;
|
|
2283
|
-
gap:
|
|
2384
|
+
gap: 6px;
|
|
2385
|
+
margin-top: 8px;
|
|
2284
2386
|
}
|
|
2285
2387
|
|
|
2286
2388
|
.result-benefits-list {
|