mantenimento-app 2.1.8 → 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 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 SCENARIO_LABELS = ["A", "B", "C"];
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
- toggleBtn.querySelector("span").textContent = logged ? `${tr("authUserPrefix")}: ${authSession.username}` : tr("authLogin");
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 >= SCENARIO_LAB_MAX) {
3994
+ if (scenarioLab.length >= getScenarioMaxForUser()) {
3934
3995
  alert(tr("scenarioLabMaxReached"));
3935
3996
  return;
3936
3997
  }
@@ -5438,10 +5499,12 @@ ${scenarioLab.length ? `
5438
5499
  document.getElementById("btnReset").addEventListener("click", resetAll);
5439
5500
 
5440
5501
  document.getElementById("btnExportJson").addEventListener("click", async () => {
5502
+ if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnExportJson")); return; }
5441
5503
  await exportJson();
5442
5504
  });
5443
5505
 
5444
5506
  document.getElementById("btnImportJson").addEventListener("click", () => {
5507
+ if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnImportJson")); return; }
5445
5508
  document.getElementById("fileJson").click();
5446
5509
  });
5447
5510
 
@@ -5454,10 +5517,12 @@ ${scenarioLab.length ? `
5454
5517
  });
5455
5518
 
5456
5519
  document.getElementById("btnPdf").addEventListener("click", () => {
5520
+ if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnPdf")); return; }
5457
5521
  exportPdfDirect();
5458
5522
  });
5459
5523
 
5460
5524
  document.getElementById("btnSaveScenario").addEventListener("click", () => {
5525
+ if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnSaveScenario")); return; }
5461
5526
  saveCurrentScenario();
5462
5527
  });
5463
5528
 
@@ -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 SCENARIO_LABELS = ["A", "B", "C"];
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
- toggleBtn.querySelector("span").textContent = logged ? `${tr("authUserPrefix")}: ${authSession.username}` : tr("authLogin");
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 >= SCENARIO_LAB_MAX) {
3994
+ if (scenarioLab.length >= getScenarioMaxForUser()) {
3934
3995
  alert(tr("scenarioLabMaxReached"));
3935
3996
  return;
3936
3997
  }
@@ -5438,10 +5499,12 @@ ${scenarioLab.length ? `
5438
5499
  document.getElementById("btnReset").addEventListener("click", resetAll);
5439
5500
 
5440
5501
  document.getElementById("btnExportJson").addEventListener("click", async () => {
5502
+ if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnExportJson")); return; }
5441
5503
  await exportJson();
5442
5504
  });
5443
5505
 
5444
5506
  document.getElementById("btnImportJson").addEventListener("click", () => {
5507
+ if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnImportJson")); return; }
5445
5508
  document.getElementById("fileJson").click();
5446
5509
  });
5447
5510
 
@@ -5454,10 +5517,12 @@ ${scenarioLab.length ? `
5454
5517
  });
5455
5518
 
5456
5519
  document.getElementById("btnPdf").addEventListener("click", () => {
5520
+ if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnPdf")); return; }
5457
5521
  exportPdfDirect();
5458
5522
  });
5459
5523
 
5460
5524
  document.getElementById("btnSaveScenario").addEventListener("click", () => {
5525
+ if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnSaveScenario")); return; }
5461
5526
  saveCurrentScenario();
5462
5527
  });
5463
5528
 
@@ -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.8"></script>
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: center;
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mantenimento-app",
3
- "version": "2.1.8",
3
+ "version": "2.1.9",
4
4
  "description": "Frontend + backend architecture for the mantenimento calculator",
5
5
  "type": "commonjs",
6
6
  "main": "backend/calculate-model.js",