mantenimento-app 2.1.8 → 2.2.0
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 +181 -8
- package/backend/calculate-model.js +16 -1
- package/frontend/public/app.js +181 -8
- package/frontend/public/index.html +17 -1
- package/frontend/public/styles.css +112 -3
- 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}",
|
|
@@ -367,6 +374,19 @@ const defaultExpenseItems = [
|
|
|
367
374
|
spiegDetailPerm: "La quota diretta dipende dai giorni di permanenza presso ciascun coniuge: quota diretta = fabbisogno x permanenza%.",
|
|
368
375
|
spiegDetailResultTransfer: "L'assegno suggerito nasce dal saldo: quota teorica del pagante - quota diretta del pagante. Se il saldo e positivo, viene trasferito all'altro coniuge.",
|
|
369
376
|
spiegDetailResultNoTransfer: "Se nessun saldo risulta positivo, il modello non suggerisce trasferimenti tra coniugi.",
|
|
377
|
+
sepCostBoxTitle: "💸 Costo della separazione",
|
|
378
|
+
sepCostBoxNote: "Stima le spese mensili che la coppia avrebbe sostenuto vivendo insieme: il modello calcolerà il costo di duplicazione generato dalla separazione e la perdita economica netta per ciascun coniuge.",
|
|
379
|
+
lblSpeseConvivenza: "Spese mensili stimate in convivenza ({currency})",
|
|
380
|
+
hintSpeseConvivenza: "Stima delle spese mensili totali della coppia se non si fosse separata: affitto/mutuo unico, una sola utenza, una sola auto, ecc. Lascia 0 per non includere questa analisi.",
|
|
381
|
+
sepCostDuplication: "Costo duplicazione mensile",
|
|
382
|
+
sepCostPanelTitle: "💔 Effetto economico della separazione",
|
|
383
|
+
sepCostNetTogether: "Netto combinato se insieme",
|
|
384
|
+
sepCostNetSeparated: "Netto combinato dopo separazione",
|
|
385
|
+
sepCostLossMonthly: "Perdita economica mensile",
|
|
386
|
+
sepCostLossAnnually: "Perdita economica annua",
|
|
387
|
+
sepCostLossSpouse: "Impatto stimato su {spouse}",
|
|
388
|
+
sepCostInlineHint: "Duplicazione mensile stimata: {amount}",
|
|
389
|
+
sepCostWarning: "Inserisci le spese mensili in convivenza nel campo sopra per attivare questa analisi.",
|
|
370
390
|
footerVisitorsTotal: "Visitatori totali",
|
|
371
391
|
footerVisitorsActive: "Visitatori attivi",
|
|
372
392
|
footerLoggedUsers: "Utenti loggati",
|
|
@@ -455,6 +475,11 @@ const defaultExpenseItems = [
|
|
|
455
475
|
authUserFallback: "user",
|
|
456
476
|
authLogoutDone: "Logout completed.",
|
|
457
477
|
authLoginRequired: "Please login first.",
|
|
478
|
+
gateNeedsAuth: "This feature is for registered users only. Log in or sign up to continue.",
|
|
479
|
+
gateNeedsAuthTitle: "Login required",
|
|
480
|
+
gateNeedsDonor: "Premium feature for donors. Support the project to unlock it.",
|
|
481
|
+
gateNeedsDonorTitle: "Premium feature",
|
|
482
|
+
gateDonorBadge: "Premium",
|
|
458
483
|
authSaveFailed: "Cloud profile save failed: {message}",
|
|
459
484
|
authCloudSaved: "Cloud profile saved. History versions: {count}.",
|
|
460
485
|
authLoadFailed: "Cloud profile load failed: {message}",
|
|
@@ -688,6 +713,19 @@ const defaultExpenseItems = [
|
|
|
688
713
|
spiegDetailPerm: "Direct share depends on permanence days with each spouse: direct share = children needs x permanence%.",
|
|
689
714
|
spiegDetailResultTransfer: "Suggested support comes from the balance: payer theoretical share - payer direct share. If the balance is positive, it is transferred to the other spouse.",
|
|
690
715
|
spiegDetailResultNoTransfer: "If no balance is positive, the model suggests no transfer between spouses.",
|
|
716
|
+
sepCostBoxTitle: "💸 Cost of separation",
|
|
717
|
+
sepCostBoxNote: "Estimate the monthly expenses the couple would have incurred if living together: the model will compute the duplication cost generated by the separation and the net economic loss per spouse.",
|
|
718
|
+
lblSpeseConvivenza: "Estimated monthly expenses when cohabiting ({currency})",
|
|
719
|
+
hintSpeseConvivenza: "Estimated total monthly expenses of the couple if they had not separated: single rent/mortgage, single utilities, single car, etc. Leave 0 to skip this analysis.",
|
|
720
|
+
sepCostDuplication: "Monthly duplication cost",
|
|
721
|
+
sepCostPanelTitle: "💔 Economic effect of separation",
|
|
722
|
+
sepCostNetTogether: "Combined net if together",
|
|
723
|
+
sepCostNetSeparated: "Combined net after separation",
|
|
724
|
+
sepCostLossMonthly: "Monthly economic loss",
|
|
725
|
+
sepCostLossAnnually: "Annual economic loss",
|
|
726
|
+
sepCostLossSpouse: "Estimated impact on {spouse}",
|
|
727
|
+
sepCostInlineHint: "Estimated monthly duplication: {amount}",
|
|
728
|
+
sepCostWarning: "Enter the cohabiting monthly expenses above to activate this analysis.",
|
|
691
729
|
footerVisitorsTotal: "Total visitors",
|
|
692
730
|
footerVisitorsActive: "Active visitors",
|
|
693
731
|
footerLoggedUsers: "Logged users",
|
|
@@ -710,7 +748,8 @@ const defaultExpenseItems = [
|
|
|
710
748
|
const authSession = {
|
|
711
749
|
username: null,
|
|
712
750
|
userId: null,
|
|
713
|
-
keyBits: null
|
|
751
|
+
keyBits: null,
|
|
752
|
+
isDonor: false
|
|
714
753
|
};
|
|
715
754
|
const authUiState = {
|
|
716
755
|
mode: "login",
|
|
@@ -1171,6 +1210,14 @@ const defaultExpenseItems = [
|
|
|
1171
1210
|
if (permLegendC2) permLegendC2.textContent = c2n();
|
|
1172
1211
|
if (extraBoxTitle) extraBoxTitle.textContent = tr("extraBoxTitle");
|
|
1173
1212
|
if (extraBoxNote) extraBoxNote.textContent = tr("extraBoxNote");
|
|
1213
|
+
const sepCostBoxTitleEl = document.getElementById("sepCostBoxTitle");
|
|
1214
|
+
const sepCostBoxNoteEl = document.getElementById("sepCostBoxNote");
|
|
1215
|
+
const lblSpeseConvivenzaEl = document.getElementById("lblSpeseConvivenza");
|
|
1216
|
+
const hintSpeseConvivenzaEl = document.getElementById("hintSpeseConvivenza");
|
|
1217
|
+
if (sepCostBoxTitleEl) sepCostBoxTitleEl.textContent = tr("sepCostBoxTitle");
|
|
1218
|
+
if (sepCostBoxNoteEl) sepCostBoxNoteEl.textContent = tr("sepCostBoxNote");
|
|
1219
|
+
if (lblSpeseConvivenzaEl) lblSpeseConvivenzaEl.textContent = msg("lblSpeseConvivenza", { currency: currentCurrency });
|
|
1220
|
+
if (hintSpeseConvivenzaEl) hintSpeseConvivenzaEl.title = tr("hintSpeseConvivenza");
|
|
1174
1221
|
if (firstHomeBoxTitle) firstHomeBoxTitle.textContent = tr("firstHomeBoxTitle");
|
|
1175
1222
|
if (firstHomeBoxNote) firstHomeBoxNote.textContent = tr("firstHomeBoxNote");
|
|
1176
1223
|
if (lblPrimaCasaMutuoEnabled) lblPrimaCasaMutuoEnabled.textContent = tr("firstHomeMortgageEnabledLabel");
|
|
@@ -1642,6 +1689,7 @@ const defaultExpenseItems = [
|
|
|
1642
1689
|
authSession.username = username;
|
|
1643
1690
|
authSession.userId = user.id;
|
|
1644
1691
|
authSession.keyBits = await deriveSessionKeyBits(password, user.id);
|
|
1692
|
+
authSession.isDonor = localStorage.getItem(`m_donor_${user.id}`) === "1";
|
|
1645
1693
|
updateAuthUi();
|
|
1646
1694
|
return msg("authLoginAs", { username });
|
|
1647
1695
|
}
|
|
@@ -1766,6 +1814,55 @@ const defaultExpenseItems = [
|
|
|
1766
1814
|
el.textContent = message;
|
|
1767
1815
|
}
|
|
1768
1816
|
|
|
1817
|
+
function isLoggedIn() { return !!authSession.username; }
|
|
1818
|
+
function isDonationPolicyBypassedUser() {
|
|
1819
|
+
return normalizeUsername(authSession.username) === "favagit";
|
|
1820
|
+
}
|
|
1821
|
+
function isDonorUser() {
|
|
1822
|
+
return !!authSession.isDonor || isDonationPolicyBypassedUser();
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
function getScenarioMaxForUser() {
|
|
1826
|
+
if (!isLoggedIn()) return 0;
|
|
1827
|
+
return isDonorUser() ? SCENARIO_LAB_MAX_DONOR : SCENARIO_LAB_MAX_FREE;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
function showAuthGateMessage(triggerEl) {
|
|
1831
|
+
setAuthMenuOpen(true);
|
|
1832
|
+
setAuthStatus(tr("gateNeedsAuth"), true);
|
|
1833
|
+
if (triggerEl) {
|
|
1834
|
+
triggerEl.classList.add("gate-flash");
|
|
1835
|
+
setTimeout(() => triggerEl.classList.remove("gate-flash"), 700);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
function showDonorGateMessage(triggerEl) {
|
|
1840
|
+
setAuthMenuOpen(true);
|
|
1841
|
+
setAuthStatus(tr("gateNeedsDonor"), true);
|
|
1842
|
+
if (triggerEl) {
|
|
1843
|
+
triggerEl.classList.add("gate-flash");
|
|
1844
|
+
setTimeout(() => triggerEl.classList.remove("gate-flash"), 700);
|
|
1845
|
+
}
|
|
1846
|
+
const donateBanner = document.querySelector(".donate-banner");
|
|
1847
|
+
if (donateBanner) setTimeout(() => donateBanner.scrollIntoView({ behavior: "smooth", block: "center" }), 200);
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
function applyFeatureGates() {
|
|
1851
|
+
const logged = isLoggedIn();
|
|
1852
|
+
const donor = isDonorUser();
|
|
1853
|
+
const authGatedIds = ["btnPdf", "btnExportJson", "btnImportJson", "btnSaveScenario"];
|
|
1854
|
+
authGatedIds.forEach((id) => {
|
|
1855
|
+
const btn = document.getElementById(id);
|
|
1856
|
+
if (!btn) return;
|
|
1857
|
+
btn.classList.toggle("gate-locked", !logged);
|
|
1858
|
+
btn.classList.toggle("gate-donor", logged && !donor && id === "btnSaveScenario" && false); // donor gate placeholder
|
|
1859
|
+
});
|
|
1860
|
+
const authMenuBtn = document.getElementById("btnAuthMenu");
|
|
1861
|
+
if (authMenuBtn) {
|
|
1862
|
+
authMenuBtn.classList.toggle("has-donor-badge", logged && donor);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1769
1866
|
function updateAuthModeUi() {
|
|
1770
1867
|
const isSignup = authUiState.mode === "signup";
|
|
1771
1868
|
const loginModeBtn = document.getElementById("btnAuthModeLogin");
|
|
@@ -1809,7 +1906,8 @@ const defaultExpenseItems = [
|
|
|
1809
1906
|
if (sessionActions) sessionActions.classList.toggle("is-hidden", !logged);
|
|
1810
1907
|
if (toggleBtn) {
|
|
1811
1908
|
toggleBtn.classList.toggle("logged", logged);
|
|
1812
|
-
|
|
1909
|
+
const badge = logged && authSession.isDonor ? ` ✦` : "";
|
|
1910
|
+
toggleBtn.querySelector("span").textContent = logged ? `${tr("authUserPrefix")}: ${authSession.username}${badge}` : tr("authLogin");
|
|
1813
1911
|
}
|
|
1814
1912
|
|
|
1815
1913
|
if (logged) {
|
|
@@ -1833,6 +1931,7 @@ const defaultExpenseItems = [
|
|
|
1833
1931
|
setAuthStatus(tr("authNotAuthenticated"), false);
|
|
1834
1932
|
}
|
|
1835
1933
|
|
|
1934
|
+
applyFeatureGates();
|
|
1836
1935
|
void syncPresenceTrackState();
|
|
1837
1936
|
}
|
|
1838
1937
|
|
|
@@ -2377,6 +2476,7 @@ const defaultExpenseItems = [
|
|
|
2377
2476
|
authSession.username = null;
|
|
2378
2477
|
authSession.userId = null;
|
|
2379
2478
|
authSession.keyBits = null;
|
|
2479
|
+
authSession.isDonor = false;
|
|
2380
2480
|
cloudProfileSession.loaded = null;
|
|
2381
2481
|
cloudProfileSession.history = [];
|
|
2382
2482
|
updateAuthUi();
|
|
@@ -3202,7 +3302,8 @@ const defaultExpenseItems = [
|
|
|
3202
3302
|
straordAnn1: num("straordAnn1"),
|
|
3203
3303
|
straordAnn2: num("straordAnn2"),
|
|
3204
3304
|
c1SpeseDetails,
|
|
3205
|
-
|
|
3305
|
+
c2SpeseDetails,
|
|
3306
|
+
speseConvivenza: num("speseConvivenza"),
|
|
3206
3307
|
c1SpeseDetailUi,
|
|
3207
3308
|
c2SpeseDetailUi,
|
|
3208
3309
|
c1Spese,
|
|
@@ -3323,7 +3424,18 @@ const defaultExpenseItems = [
|
|
|
3323
3424
|
const post1 = disp1 - assegnoDa1a2 + assegnoDa2a1;
|
|
3324
3425
|
const post2 = disp2 - assegnoDa2a1 + assegnoDa1a2;
|
|
3325
3426
|
|
|
3326
|
-
|
|
3427
|
+
// Separation cost analysis (only active when speseConvivenza > 0)
|
|
3428
|
+
const speseConvivenza = Math.max(0, Number(payload.speseConvivenza || 0));
|
|
3429
|
+
const costoSeparazioneMensile = speseConvivenza > 0 ? speseTot - speseConvivenza : null;
|
|
3430
|
+
const nettoInsiemeCombinato = speseConvivenza > 0 ? (r1 + r2 - speseConvivenza) : null;
|
|
3431
|
+
const nettoSeparatoTotale = post1 + post2;
|
|
3432
|
+
const perditaMensile = nettoInsiemeCombinato !== null ? nettoInsiemeCombinato - nettoSeparatoTotale : null;
|
|
3433
|
+
const perditaAnnua = perditaMensile !== null ? perditaMensile * 12 : null;
|
|
3434
|
+
const totReddito = Math.max(0.001, r1 + r2);
|
|
3435
|
+
const perditaSpouse1 = perditaMensile !== null ? perditaMensile * (r1 / totReddito) : null;
|
|
3436
|
+
const perditaSpouse2 = perditaMensile !== null ? perditaMensile * (r2 / totReddito) : null;
|
|
3437
|
+
|
|
3438
|
+
return {
|
|
3327
3439
|
r1, r2, r1Raw, r2Raw, incomeMode, figli, perm1, perm2,
|
|
3328
3440
|
aPerc1, aPag1, aPerc2, aPag2, aFam1, aFam2,
|
|
3329
3441
|
match12, match21, esternoPag1, esternoPag2,
|
|
@@ -3341,7 +3453,11 @@ const defaultExpenseItems = [
|
|
|
3341
3453
|
primaCasaConsidered, primaCasaTransfer1to2, primaCasaTransfer2to1,
|
|
3342
3454
|
compensativeBenefits,
|
|
3343
3455
|
assegnoDa1a2, assegnoDa2a1,
|
|
3344
|
-
post1, post2
|
|
3456
|
+
post1, post2,
|
|
3457
|
+
speseConvivenza, costoSeparazioneMensile,
|
|
3458
|
+
nettoInsiemeCombinato, nettoSeparatoTotale,
|
|
3459
|
+
perditaMensile, perditaAnnua,
|
|
3460
|
+
perditaSpouse1, perditaSpouse2
|
|
3345
3461
|
};
|
|
3346
3462
|
}
|
|
3347
3463
|
|
|
@@ -3930,7 +4046,7 @@ const defaultExpenseItems = [
|
|
|
3930
4046
|
}
|
|
3931
4047
|
|
|
3932
4048
|
function saveCurrentScenario() {
|
|
3933
|
-
if (scenarioLab.length >=
|
|
4049
|
+
if (scenarioLab.length >= getScenarioMaxForUser()) {
|
|
3934
4050
|
alert(tr("scenarioLabMaxReached"));
|
|
3935
4051
|
return;
|
|
3936
4052
|
}
|
|
@@ -4465,6 +4581,57 @@ const defaultExpenseItems = [
|
|
|
4465
4581
|
});
|
|
4466
4582
|
}
|
|
4467
4583
|
|
|
4584
|
+
function renderSeparationCostPanel(m) {
|
|
4585
|
+
const panel = document.getElementById("sepCostPanel");
|
|
4586
|
+
if (!panel) return;
|
|
4587
|
+
|
|
4588
|
+
// Update inline hint showing duplication cost
|
|
4589
|
+
const hintDiv = document.getElementById("speseConvivenzaHint");
|
|
4590
|
+
if (hintDiv) {
|
|
4591
|
+
hintDiv.textContent = m.speseConvivenza > 0 && m.costoSeparazioneMensile !== null
|
|
4592
|
+
? msg("sepCostInlineHint", { amount: eur(m.costoSeparazioneMensile) })
|
|
4593
|
+
: "";
|
|
4594
|
+
}
|
|
4595
|
+
|
|
4596
|
+
if (!m.speseConvivenza || m.speseConvivenza <= 0) {
|
|
4597
|
+
panel.innerHTML = `<div class="sep-cost-warning">${escapeHtml(tr("sepCostWarning"))}</div>`;
|
|
4598
|
+
return;
|
|
4599
|
+
}
|
|
4600
|
+
|
|
4601
|
+
const c1Name = c1n();
|
|
4602
|
+
const c2Name = c2n();
|
|
4603
|
+
const c1NameEsc = escapeHtml(c1Name);
|
|
4604
|
+
const c2NameEsc = escapeHtml(c2Name);
|
|
4605
|
+
|
|
4606
|
+
const lossClass = (v) => v === null ? "" : v > 0 ? "sep-loss-negative" : "sep-loss-positive";
|
|
4607
|
+
const rowHtml = (label, value, em) => {
|
|
4608
|
+
const cls = lossClass(value);
|
|
4609
|
+
const formatted = value === null ? "—" : eur(value);
|
|
4610
|
+
return `<div class="sep-cost-row${em ? " sep-cost-row--em" : ""}">
|
|
4611
|
+
<span class="sep-cost-label">${label}</span>
|
|
4612
|
+
<strong class="sep-cost-value ${cls}">${formatted}</strong>
|
|
4613
|
+
</div>`;
|
|
4614
|
+
};
|
|
4615
|
+
|
|
4616
|
+
panel.innerHTML = `
|
|
4617
|
+
<div class="sep-cost-panel">
|
|
4618
|
+
<h3 class="sep-cost-title">${escapeHtml(tr("sepCostPanelTitle"))}</h3>
|
|
4619
|
+
<div class="sep-cost-section">
|
|
4620
|
+
${rowHtml(tr("sepCostNetTogether"), m.nettoInsiemeCombinato, false)}
|
|
4621
|
+
${rowHtml(tr("sepCostNetSeparated"), m.nettoSeparatoTotale, false)}
|
|
4622
|
+
</div>
|
|
4623
|
+
<div class="sep-cost-divider"></div>
|
|
4624
|
+
<div class="sep-cost-section">
|
|
4625
|
+
${rowHtml(tr("sepCostDuplication"), m.costoSeparazioneMensile, false)}
|
|
4626
|
+
${rowHtml(tr("sepCostLossMonthly"), m.perditaMensile, true)}
|
|
4627
|
+
${rowHtml(tr("sepCostLossAnnually"), m.perditaAnnua, true)}
|
|
4628
|
+
${rowHtml(msg("sepCostLossSpouse", { spouse: c1NameEsc }), m.perditaSpouse1, false)}
|
|
4629
|
+
${rowHtml(msg("sepCostLossSpouse", { spouse: c2NameEsc }), m.perditaSpouse2, false)}
|
|
4630
|
+
</div>
|
|
4631
|
+
</div>
|
|
4632
|
+
`;
|
|
4633
|
+
}
|
|
4634
|
+
|
|
4468
4635
|
function renderAll() {
|
|
4469
4636
|
const m = computeModel();
|
|
4470
4637
|
updateExtraordinaryModuleUi();
|
|
@@ -4472,6 +4639,7 @@ const defaultExpenseItems = [
|
|
|
4472
4639
|
renderLivePanel(m);
|
|
4473
4640
|
calculate(m);
|
|
4474
4641
|
renderSpiegabilita(m);
|
|
4642
|
+
renderSeparationCostPanel(m);
|
|
4475
4643
|
renderScenarioLab();
|
|
4476
4644
|
}
|
|
4477
4645
|
|
|
@@ -5303,7 +5471,8 @@ ${scenarioLab.length ? `
|
|
|
5303
5471
|
primaCasaAssegnataA: String(document.getElementById("primaCasaAssegnataA")?.value || ""),
|
|
5304
5472
|
primaCasaMutuoPerc1: num("primaCasaMutuoPerc1"),
|
|
5305
5473
|
straordAnn1: num("straordAnn1"),
|
|
5306
|
-
straordAnn2: num("straordAnn2")
|
|
5474
|
+
straordAnn2: num("straordAnn2"),
|
|
5475
|
+
speseConvivenza: num("speseConvivenza")
|
|
5307
5476
|
};
|
|
5308
5477
|
const spese = expenseItems.map((_, i) => ({
|
|
5309
5478
|
c1: num(`c1_${i}`),
|
|
@@ -5438,10 +5607,12 @@ ${scenarioLab.length ? `
|
|
|
5438
5607
|
document.getElementById("btnReset").addEventListener("click", resetAll);
|
|
5439
5608
|
|
|
5440
5609
|
document.getElementById("btnExportJson").addEventListener("click", async () => {
|
|
5610
|
+
if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnExportJson")); return; }
|
|
5441
5611
|
await exportJson();
|
|
5442
5612
|
});
|
|
5443
5613
|
|
|
5444
5614
|
document.getElementById("btnImportJson").addEventListener("click", () => {
|
|
5615
|
+
if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnImportJson")); return; }
|
|
5445
5616
|
document.getElementById("fileJson").click();
|
|
5446
5617
|
});
|
|
5447
5618
|
|
|
@@ -5454,10 +5625,12 @@ ${scenarioLab.length ? `
|
|
|
5454
5625
|
});
|
|
5455
5626
|
|
|
5456
5627
|
document.getElementById("btnPdf").addEventListener("click", () => {
|
|
5628
|
+
if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnPdf")); return; }
|
|
5457
5629
|
exportPdfDirect();
|
|
5458
5630
|
});
|
|
5459
5631
|
|
|
5460
5632
|
document.getElementById("btnSaveScenario").addEventListener("click", () => {
|
|
5633
|
+
if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnSaveScenario")); return; }
|
|
5461
5634
|
saveCurrentScenario();
|
|
5462
5635
|
});
|
|
5463
5636
|
|
|
@@ -125,6 +125,17 @@ function calculateModel(input) {
|
|
|
125
125
|
const post1 = disp1 - assegnoDa1a2 + assegnoDa2a1;
|
|
126
126
|
const post2 = disp2 - assegnoDa2a1 + assegnoDa1a2;
|
|
127
127
|
|
|
128
|
+
// Separation cost analysis (only active when speseConvivenza > 0)
|
|
129
|
+
const speseConvivenza = Math.max(0, toNumber(input.speseConvivenza));
|
|
130
|
+
const costoSeparazioneMensile = speseConvivenza > 0 ? speseTot - speseConvivenza : null;
|
|
131
|
+
const nettoInsiemeCombinato = speseConvivenza > 0 ? (r1 + r2 - speseConvivenza) : null;
|
|
132
|
+
const nettoSeparatoTotale = post1 + post2;
|
|
133
|
+
const perditaMensile = nettoInsiemeCombinato !== null ? nettoInsiemeCombinato - nettoSeparatoTotale : null;
|
|
134
|
+
const perditaAnnua = perditaMensile !== null ? perditaMensile * 12 : null;
|
|
135
|
+
const totReddito = Math.max(0.001, r1 + r2);
|
|
136
|
+
const perditaSpouse1 = perditaMensile !== null ? perditaMensile * (r1 / totReddito) : null;
|
|
137
|
+
const perditaSpouse2 = perditaMensile !== null ? perditaMensile * (r2 / totReddito) : null;
|
|
138
|
+
|
|
128
139
|
return {
|
|
129
140
|
r1, r2, r1Raw, r2Raw, incomeMode, figli, perm1, perm2,
|
|
130
141
|
aPerc1, aPag1, aPerc2, aPag2, aFam1, aFam2,
|
|
@@ -144,7 +155,11 @@ function calculateModel(input) {
|
|
|
144
155
|
primaCasaConsidered, primaCasaTransfer1to2, primaCasaTransfer2to1,
|
|
145
156
|
compensativeBenefits,
|
|
146
157
|
assegnoDa1a2, assegnoDa2a1,
|
|
147
|
-
post1, post2
|
|
158
|
+
post1, post2,
|
|
159
|
+
speseConvivenza, costoSeparazioneMensile,
|
|
160
|
+
nettoInsiemeCombinato, nettoSeparatoTotale,
|
|
161
|
+
perditaMensile, perditaAnnua,
|
|
162
|
+
perditaSpouse1, perditaSpouse2
|
|
148
163
|
};
|
|
149
164
|
}
|
|
150
165
|
|
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}",
|
|
@@ -367,6 +374,19 @@ const defaultExpenseItems = [
|
|
|
367
374
|
spiegDetailPerm: "La quota diretta dipende dai giorni di permanenza presso ciascun coniuge: quota diretta = fabbisogno x permanenza%.",
|
|
368
375
|
spiegDetailResultTransfer: "L'assegno suggerito nasce dal saldo: quota teorica del pagante - quota diretta del pagante. Se il saldo e positivo, viene trasferito all'altro coniuge.",
|
|
369
376
|
spiegDetailResultNoTransfer: "Se nessun saldo risulta positivo, il modello non suggerisce trasferimenti tra coniugi.",
|
|
377
|
+
sepCostBoxTitle: "💸 Costo della separazione",
|
|
378
|
+
sepCostBoxNote: "Stima le spese mensili che la coppia avrebbe sostenuto vivendo insieme: il modello calcolerà il costo di duplicazione generato dalla separazione e la perdita economica netta per ciascun coniuge.",
|
|
379
|
+
lblSpeseConvivenza: "Spese mensili stimate in convivenza ({currency})",
|
|
380
|
+
hintSpeseConvivenza: "Stima delle spese mensili totali della coppia se non si fosse separata: affitto/mutuo unico, una sola utenza, una sola auto, ecc. Lascia 0 per non includere questa analisi.",
|
|
381
|
+
sepCostDuplication: "Costo duplicazione mensile",
|
|
382
|
+
sepCostPanelTitle: "💔 Effetto economico della separazione",
|
|
383
|
+
sepCostNetTogether: "Netto combinato se insieme",
|
|
384
|
+
sepCostNetSeparated: "Netto combinato dopo separazione",
|
|
385
|
+
sepCostLossMonthly: "Perdita economica mensile",
|
|
386
|
+
sepCostLossAnnually: "Perdita economica annua",
|
|
387
|
+
sepCostLossSpouse: "Impatto stimato su {spouse}",
|
|
388
|
+
sepCostInlineHint: "Duplicazione mensile stimata: {amount}",
|
|
389
|
+
sepCostWarning: "Inserisci le spese mensili in convivenza nel campo sopra per attivare questa analisi.",
|
|
370
390
|
footerVisitorsTotal: "Visitatori totali",
|
|
371
391
|
footerVisitorsActive: "Visitatori attivi",
|
|
372
392
|
footerLoggedUsers: "Utenti loggati",
|
|
@@ -455,6 +475,11 @@ const defaultExpenseItems = [
|
|
|
455
475
|
authUserFallback: "user",
|
|
456
476
|
authLogoutDone: "Logout completed.",
|
|
457
477
|
authLoginRequired: "Please login first.",
|
|
478
|
+
gateNeedsAuth: "This feature is for registered users only. Log in or sign up to continue.",
|
|
479
|
+
gateNeedsAuthTitle: "Login required",
|
|
480
|
+
gateNeedsDonor: "Premium feature for donors. Support the project to unlock it.",
|
|
481
|
+
gateNeedsDonorTitle: "Premium feature",
|
|
482
|
+
gateDonorBadge: "Premium",
|
|
458
483
|
authSaveFailed: "Cloud profile save failed: {message}",
|
|
459
484
|
authCloudSaved: "Cloud profile saved. History versions: {count}.",
|
|
460
485
|
authLoadFailed: "Cloud profile load failed: {message}",
|
|
@@ -688,6 +713,19 @@ const defaultExpenseItems = [
|
|
|
688
713
|
spiegDetailPerm: "Direct share depends on permanence days with each spouse: direct share = children needs x permanence%.",
|
|
689
714
|
spiegDetailResultTransfer: "Suggested support comes from the balance: payer theoretical share - payer direct share. If the balance is positive, it is transferred to the other spouse.",
|
|
690
715
|
spiegDetailResultNoTransfer: "If no balance is positive, the model suggests no transfer between spouses.",
|
|
716
|
+
sepCostBoxTitle: "💸 Cost of separation",
|
|
717
|
+
sepCostBoxNote: "Estimate the monthly expenses the couple would have incurred if living together: the model will compute the duplication cost generated by the separation and the net economic loss per spouse.",
|
|
718
|
+
lblSpeseConvivenza: "Estimated monthly expenses when cohabiting ({currency})",
|
|
719
|
+
hintSpeseConvivenza: "Estimated total monthly expenses of the couple if they had not separated: single rent/mortgage, single utilities, single car, etc. Leave 0 to skip this analysis.",
|
|
720
|
+
sepCostDuplication: "Monthly duplication cost",
|
|
721
|
+
sepCostPanelTitle: "💔 Economic effect of separation",
|
|
722
|
+
sepCostNetTogether: "Combined net if together",
|
|
723
|
+
sepCostNetSeparated: "Combined net after separation",
|
|
724
|
+
sepCostLossMonthly: "Monthly economic loss",
|
|
725
|
+
sepCostLossAnnually: "Annual economic loss",
|
|
726
|
+
sepCostLossSpouse: "Estimated impact on {spouse}",
|
|
727
|
+
sepCostInlineHint: "Estimated monthly duplication: {amount}",
|
|
728
|
+
sepCostWarning: "Enter the cohabiting monthly expenses above to activate this analysis.",
|
|
691
729
|
footerVisitorsTotal: "Total visitors",
|
|
692
730
|
footerVisitorsActive: "Active visitors",
|
|
693
731
|
footerLoggedUsers: "Logged users",
|
|
@@ -710,7 +748,8 @@ const defaultExpenseItems = [
|
|
|
710
748
|
const authSession = {
|
|
711
749
|
username: null,
|
|
712
750
|
userId: null,
|
|
713
|
-
keyBits: null
|
|
751
|
+
keyBits: null,
|
|
752
|
+
isDonor: false
|
|
714
753
|
};
|
|
715
754
|
const authUiState = {
|
|
716
755
|
mode: "login",
|
|
@@ -1171,6 +1210,14 @@ const defaultExpenseItems = [
|
|
|
1171
1210
|
if (permLegendC2) permLegendC2.textContent = c2n();
|
|
1172
1211
|
if (extraBoxTitle) extraBoxTitle.textContent = tr("extraBoxTitle");
|
|
1173
1212
|
if (extraBoxNote) extraBoxNote.textContent = tr("extraBoxNote");
|
|
1213
|
+
const sepCostBoxTitleEl = document.getElementById("sepCostBoxTitle");
|
|
1214
|
+
const sepCostBoxNoteEl = document.getElementById("sepCostBoxNote");
|
|
1215
|
+
const lblSpeseConvivenzaEl = document.getElementById("lblSpeseConvivenza");
|
|
1216
|
+
const hintSpeseConvivenzaEl = document.getElementById("hintSpeseConvivenza");
|
|
1217
|
+
if (sepCostBoxTitleEl) sepCostBoxTitleEl.textContent = tr("sepCostBoxTitle");
|
|
1218
|
+
if (sepCostBoxNoteEl) sepCostBoxNoteEl.textContent = tr("sepCostBoxNote");
|
|
1219
|
+
if (lblSpeseConvivenzaEl) lblSpeseConvivenzaEl.textContent = msg("lblSpeseConvivenza", { currency: currentCurrency });
|
|
1220
|
+
if (hintSpeseConvivenzaEl) hintSpeseConvivenzaEl.title = tr("hintSpeseConvivenza");
|
|
1174
1221
|
if (firstHomeBoxTitle) firstHomeBoxTitle.textContent = tr("firstHomeBoxTitle");
|
|
1175
1222
|
if (firstHomeBoxNote) firstHomeBoxNote.textContent = tr("firstHomeBoxNote");
|
|
1176
1223
|
if (lblPrimaCasaMutuoEnabled) lblPrimaCasaMutuoEnabled.textContent = tr("firstHomeMortgageEnabledLabel");
|
|
@@ -1642,6 +1689,7 @@ const defaultExpenseItems = [
|
|
|
1642
1689
|
authSession.username = username;
|
|
1643
1690
|
authSession.userId = user.id;
|
|
1644
1691
|
authSession.keyBits = await deriveSessionKeyBits(password, user.id);
|
|
1692
|
+
authSession.isDonor = localStorage.getItem(`m_donor_${user.id}`) === "1";
|
|
1645
1693
|
updateAuthUi();
|
|
1646
1694
|
return msg("authLoginAs", { username });
|
|
1647
1695
|
}
|
|
@@ -1766,6 +1814,55 @@ const defaultExpenseItems = [
|
|
|
1766
1814
|
el.textContent = message;
|
|
1767
1815
|
}
|
|
1768
1816
|
|
|
1817
|
+
function isLoggedIn() { return !!authSession.username; }
|
|
1818
|
+
function isDonationPolicyBypassedUser() {
|
|
1819
|
+
return normalizeUsername(authSession.username) === "favagit";
|
|
1820
|
+
}
|
|
1821
|
+
function isDonorUser() {
|
|
1822
|
+
return !!authSession.isDonor || isDonationPolicyBypassedUser();
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
function getScenarioMaxForUser() {
|
|
1826
|
+
if (!isLoggedIn()) return 0;
|
|
1827
|
+
return isDonorUser() ? SCENARIO_LAB_MAX_DONOR : SCENARIO_LAB_MAX_FREE;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
function showAuthGateMessage(triggerEl) {
|
|
1831
|
+
setAuthMenuOpen(true);
|
|
1832
|
+
setAuthStatus(tr("gateNeedsAuth"), true);
|
|
1833
|
+
if (triggerEl) {
|
|
1834
|
+
triggerEl.classList.add("gate-flash");
|
|
1835
|
+
setTimeout(() => triggerEl.classList.remove("gate-flash"), 700);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
function showDonorGateMessage(triggerEl) {
|
|
1840
|
+
setAuthMenuOpen(true);
|
|
1841
|
+
setAuthStatus(tr("gateNeedsDonor"), true);
|
|
1842
|
+
if (triggerEl) {
|
|
1843
|
+
triggerEl.classList.add("gate-flash");
|
|
1844
|
+
setTimeout(() => triggerEl.classList.remove("gate-flash"), 700);
|
|
1845
|
+
}
|
|
1846
|
+
const donateBanner = document.querySelector(".donate-banner");
|
|
1847
|
+
if (donateBanner) setTimeout(() => donateBanner.scrollIntoView({ behavior: "smooth", block: "center" }), 200);
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
function applyFeatureGates() {
|
|
1851
|
+
const logged = isLoggedIn();
|
|
1852
|
+
const donor = isDonorUser();
|
|
1853
|
+
const authGatedIds = ["btnPdf", "btnExportJson", "btnImportJson", "btnSaveScenario"];
|
|
1854
|
+
authGatedIds.forEach((id) => {
|
|
1855
|
+
const btn = document.getElementById(id);
|
|
1856
|
+
if (!btn) return;
|
|
1857
|
+
btn.classList.toggle("gate-locked", !logged);
|
|
1858
|
+
btn.classList.toggle("gate-donor", logged && !donor && id === "btnSaveScenario" && false); // donor gate placeholder
|
|
1859
|
+
});
|
|
1860
|
+
const authMenuBtn = document.getElementById("btnAuthMenu");
|
|
1861
|
+
if (authMenuBtn) {
|
|
1862
|
+
authMenuBtn.classList.toggle("has-donor-badge", logged && donor);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1769
1866
|
function updateAuthModeUi() {
|
|
1770
1867
|
const isSignup = authUiState.mode === "signup";
|
|
1771
1868
|
const loginModeBtn = document.getElementById("btnAuthModeLogin");
|
|
@@ -1809,7 +1906,8 @@ const defaultExpenseItems = [
|
|
|
1809
1906
|
if (sessionActions) sessionActions.classList.toggle("is-hidden", !logged);
|
|
1810
1907
|
if (toggleBtn) {
|
|
1811
1908
|
toggleBtn.classList.toggle("logged", logged);
|
|
1812
|
-
|
|
1909
|
+
const badge = logged && authSession.isDonor ? ` ✦` : "";
|
|
1910
|
+
toggleBtn.querySelector("span").textContent = logged ? `${tr("authUserPrefix")}: ${authSession.username}${badge}` : tr("authLogin");
|
|
1813
1911
|
}
|
|
1814
1912
|
|
|
1815
1913
|
if (logged) {
|
|
@@ -1833,6 +1931,7 @@ const defaultExpenseItems = [
|
|
|
1833
1931
|
setAuthStatus(tr("authNotAuthenticated"), false);
|
|
1834
1932
|
}
|
|
1835
1933
|
|
|
1934
|
+
applyFeatureGates();
|
|
1836
1935
|
void syncPresenceTrackState();
|
|
1837
1936
|
}
|
|
1838
1937
|
|
|
@@ -2377,6 +2476,7 @@ const defaultExpenseItems = [
|
|
|
2377
2476
|
authSession.username = null;
|
|
2378
2477
|
authSession.userId = null;
|
|
2379
2478
|
authSession.keyBits = null;
|
|
2479
|
+
authSession.isDonor = false;
|
|
2380
2480
|
cloudProfileSession.loaded = null;
|
|
2381
2481
|
cloudProfileSession.history = [];
|
|
2382
2482
|
updateAuthUi();
|
|
@@ -3202,7 +3302,8 @@ const defaultExpenseItems = [
|
|
|
3202
3302
|
straordAnn1: num("straordAnn1"),
|
|
3203
3303
|
straordAnn2: num("straordAnn2"),
|
|
3204
3304
|
c1SpeseDetails,
|
|
3205
|
-
|
|
3305
|
+
c2SpeseDetails,
|
|
3306
|
+
speseConvivenza: num("speseConvivenza"),
|
|
3206
3307
|
c1SpeseDetailUi,
|
|
3207
3308
|
c2SpeseDetailUi,
|
|
3208
3309
|
c1Spese,
|
|
@@ -3323,7 +3424,18 @@ const defaultExpenseItems = [
|
|
|
3323
3424
|
const post1 = disp1 - assegnoDa1a2 + assegnoDa2a1;
|
|
3324
3425
|
const post2 = disp2 - assegnoDa2a1 + assegnoDa1a2;
|
|
3325
3426
|
|
|
3326
|
-
|
|
3427
|
+
// Separation cost analysis (only active when speseConvivenza > 0)
|
|
3428
|
+
const speseConvivenza = Math.max(0, Number(payload.speseConvivenza || 0));
|
|
3429
|
+
const costoSeparazioneMensile = speseConvivenza > 0 ? speseTot - speseConvivenza : null;
|
|
3430
|
+
const nettoInsiemeCombinato = speseConvivenza > 0 ? (r1 + r2 - speseConvivenza) : null;
|
|
3431
|
+
const nettoSeparatoTotale = post1 + post2;
|
|
3432
|
+
const perditaMensile = nettoInsiemeCombinato !== null ? nettoInsiemeCombinato - nettoSeparatoTotale : null;
|
|
3433
|
+
const perditaAnnua = perditaMensile !== null ? perditaMensile * 12 : null;
|
|
3434
|
+
const totReddito = Math.max(0.001, r1 + r2);
|
|
3435
|
+
const perditaSpouse1 = perditaMensile !== null ? perditaMensile * (r1 / totReddito) : null;
|
|
3436
|
+
const perditaSpouse2 = perditaMensile !== null ? perditaMensile * (r2 / totReddito) : null;
|
|
3437
|
+
|
|
3438
|
+
return {
|
|
3327
3439
|
r1, r2, r1Raw, r2Raw, incomeMode, figli, perm1, perm2,
|
|
3328
3440
|
aPerc1, aPag1, aPerc2, aPag2, aFam1, aFam2,
|
|
3329
3441
|
match12, match21, esternoPag1, esternoPag2,
|
|
@@ -3341,7 +3453,11 @@ const defaultExpenseItems = [
|
|
|
3341
3453
|
primaCasaConsidered, primaCasaTransfer1to2, primaCasaTransfer2to1,
|
|
3342
3454
|
compensativeBenefits,
|
|
3343
3455
|
assegnoDa1a2, assegnoDa2a1,
|
|
3344
|
-
post1, post2
|
|
3456
|
+
post1, post2,
|
|
3457
|
+
speseConvivenza, costoSeparazioneMensile,
|
|
3458
|
+
nettoInsiemeCombinato, nettoSeparatoTotale,
|
|
3459
|
+
perditaMensile, perditaAnnua,
|
|
3460
|
+
perditaSpouse1, perditaSpouse2
|
|
3345
3461
|
};
|
|
3346
3462
|
}
|
|
3347
3463
|
|
|
@@ -3930,7 +4046,7 @@ const defaultExpenseItems = [
|
|
|
3930
4046
|
}
|
|
3931
4047
|
|
|
3932
4048
|
function saveCurrentScenario() {
|
|
3933
|
-
if (scenarioLab.length >=
|
|
4049
|
+
if (scenarioLab.length >= getScenarioMaxForUser()) {
|
|
3934
4050
|
alert(tr("scenarioLabMaxReached"));
|
|
3935
4051
|
return;
|
|
3936
4052
|
}
|
|
@@ -4465,6 +4581,57 @@ const defaultExpenseItems = [
|
|
|
4465
4581
|
});
|
|
4466
4582
|
}
|
|
4467
4583
|
|
|
4584
|
+
function renderSeparationCostPanel(m) {
|
|
4585
|
+
const panel = document.getElementById("sepCostPanel");
|
|
4586
|
+
if (!panel) return;
|
|
4587
|
+
|
|
4588
|
+
// Update inline hint showing duplication cost
|
|
4589
|
+
const hintDiv = document.getElementById("speseConvivenzaHint");
|
|
4590
|
+
if (hintDiv) {
|
|
4591
|
+
hintDiv.textContent = m.speseConvivenza > 0 && m.costoSeparazioneMensile !== null
|
|
4592
|
+
? msg("sepCostInlineHint", { amount: eur(m.costoSeparazioneMensile) })
|
|
4593
|
+
: "";
|
|
4594
|
+
}
|
|
4595
|
+
|
|
4596
|
+
if (!m.speseConvivenza || m.speseConvivenza <= 0) {
|
|
4597
|
+
panel.innerHTML = `<div class="sep-cost-warning">${escapeHtml(tr("sepCostWarning"))}</div>`;
|
|
4598
|
+
return;
|
|
4599
|
+
}
|
|
4600
|
+
|
|
4601
|
+
const c1Name = c1n();
|
|
4602
|
+
const c2Name = c2n();
|
|
4603
|
+
const c1NameEsc = escapeHtml(c1Name);
|
|
4604
|
+
const c2NameEsc = escapeHtml(c2Name);
|
|
4605
|
+
|
|
4606
|
+
const lossClass = (v) => v === null ? "" : v > 0 ? "sep-loss-negative" : "sep-loss-positive";
|
|
4607
|
+
const rowHtml = (label, value, em) => {
|
|
4608
|
+
const cls = lossClass(value);
|
|
4609
|
+
const formatted = value === null ? "—" : eur(value);
|
|
4610
|
+
return `<div class="sep-cost-row${em ? " sep-cost-row--em" : ""}">
|
|
4611
|
+
<span class="sep-cost-label">${label}</span>
|
|
4612
|
+
<strong class="sep-cost-value ${cls}">${formatted}</strong>
|
|
4613
|
+
</div>`;
|
|
4614
|
+
};
|
|
4615
|
+
|
|
4616
|
+
panel.innerHTML = `
|
|
4617
|
+
<div class="sep-cost-panel">
|
|
4618
|
+
<h3 class="sep-cost-title">${escapeHtml(tr("sepCostPanelTitle"))}</h3>
|
|
4619
|
+
<div class="sep-cost-section">
|
|
4620
|
+
${rowHtml(tr("sepCostNetTogether"), m.nettoInsiemeCombinato, false)}
|
|
4621
|
+
${rowHtml(tr("sepCostNetSeparated"), m.nettoSeparatoTotale, false)}
|
|
4622
|
+
</div>
|
|
4623
|
+
<div class="sep-cost-divider"></div>
|
|
4624
|
+
<div class="sep-cost-section">
|
|
4625
|
+
${rowHtml(tr("sepCostDuplication"), m.costoSeparazioneMensile, false)}
|
|
4626
|
+
${rowHtml(tr("sepCostLossMonthly"), m.perditaMensile, true)}
|
|
4627
|
+
${rowHtml(tr("sepCostLossAnnually"), m.perditaAnnua, true)}
|
|
4628
|
+
${rowHtml(msg("sepCostLossSpouse", { spouse: c1NameEsc }), m.perditaSpouse1, false)}
|
|
4629
|
+
${rowHtml(msg("sepCostLossSpouse", { spouse: c2NameEsc }), m.perditaSpouse2, false)}
|
|
4630
|
+
</div>
|
|
4631
|
+
</div>
|
|
4632
|
+
`;
|
|
4633
|
+
}
|
|
4634
|
+
|
|
4468
4635
|
function renderAll() {
|
|
4469
4636
|
const m = computeModel();
|
|
4470
4637
|
updateExtraordinaryModuleUi();
|
|
@@ -4472,6 +4639,7 @@ const defaultExpenseItems = [
|
|
|
4472
4639
|
renderLivePanel(m);
|
|
4473
4640
|
calculate(m);
|
|
4474
4641
|
renderSpiegabilita(m);
|
|
4642
|
+
renderSeparationCostPanel(m);
|
|
4475
4643
|
renderScenarioLab();
|
|
4476
4644
|
}
|
|
4477
4645
|
|
|
@@ -5303,7 +5471,8 @@ ${scenarioLab.length ? `
|
|
|
5303
5471
|
primaCasaAssegnataA: String(document.getElementById("primaCasaAssegnataA")?.value || ""),
|
|
5304
5472
|
primaCasaMutuoPerc1: num("primaCasaMutuoPerc1"),
|
|
5305
5473
|
straordAnn1: num("straordAnn1"),
|
|
5306
|
-
straordAnn2: num("straordAnn2")
|
|
5474
|
+
straordAnn2: num("straordAnn2"),
|
|
5475
|
+
speseConvivenza: num("speseConvivenza")
|
|
5307
5476
|
};
|
|
5308
5477
|
const spese = expenseItems.map((_, i) => ({
|
|
5309
5478
|
c1: num(`c1_${i}`),
|
|
@@ -5438,10 +5607,12 @@ ${scenarioLab.length ? `
|
|
|
5438
5607
|
document.getElementById("btnReset").addEventListener("click", resetAll);
|
|
5439
5608
|
|
|
5440
5609
|
document.getElementById("btnExportJson").addEventListener("click", async () => {
|
|
5610
|
+
if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnExportJson")); return; }
|
|
5441
5611
|
await exportJson();
|
|
5442
5612
|
});
|
|
5443
5613
|
|
|
5444
5614
|
document.getElementById("btnImportJson").addEventListener("click", () => {
|
|
5615
|
+
if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnImportJson")); return; }
|
|
5445
5616
|
document.getElementById("fileJson").click();
|
|
5446
5617
|
});
|
|
5447
5618
|
|
|
@@ -5454,10 +5625,12 @@ ${scenarioLab.length ? `
|
|
|
5454
5625
|
});
|
|
5455
5626
|
|
|
5456
5627
|
document.getElementById("btnPdf").addEventListener("click", () => {
|
|
5628
|
+
if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnPdf")); return; }
|
|
5457
5629
|
exportPdfDirect();
|
|
5458
5630
|
});
|
|
5459
5631
|
|
|
5460
5632
|
document.getElementById("btnSaveScenario").addEventListener("click", () => {
|
|
5633
|
+
if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnSaveScenario")); return; }
|
|
5461
5634
|
saveCurrentScenario();
|
|
5462
5635
|
});
|
|
5463
5636
|
|
|
@@ -414,6 +414,20 @@
|
|
|
414
414
|
</div>
|
|
415
415
|
</div>
|
|
416
416
|
|
|
417
|
+
<div class="extra-box extra-box-sep-cost" id="sepCostBox">
|
|
418
|
+
<div class="extra-box-title" id="sepCostBoxTitle">💸 Costo della separazione</div>
|
|
419
|
+
<div class="extra-box-note" id="sepCostBoxNote">Stima le spese mensili che la coppia avrebbe sostenuto vivendo insieme: il modello calcolerà il costo di duplicazione generato dalla separazione e la perdita economica netta per ciascun coniuge.</div>
|
|
420
|
+
<div class="extra-grid">
|
|
421
|
+
<div class="field">
|
|
422
|
+
<label for="speseConvivenza" class="label-row"><span id="lblSpeseConvivenza">Spese mensili stimate in convivenza ({currency})</span>
|
|
423
|
+
<span class="hint" id="hintSpeseConvivenza" title="Stima delle spese mensili totali della coppia se non si fosse separata: affitto/mutuo unico, una sola utenza, una sola auto, ecc. Lascia 0 per non includere questa analisi.">i</span>
|
|
424
|
+
</label>
|
|
425
|
+
<input id="speseConvivenza" type="number" min="0" step="50" value="0" />
|
|
426
|
+
<div class="extra-monthly" id="speseConvivenzaHint" style="color:#c05a00"></div>
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
|
|
417
431
|
|
|
418
432
|
<p class="spese-count-title" id="speseCountNote">
|
|
419
433
|
Elenco spese compilabili.
|
|
@@ -481,6 +495,8 @@
|
|
|
481
495
|
<div id="liveBreakdown"></div>
|
|
482
496
|
</div>
|
|
483
497
|
|
|
498
|
+
<div id="sepCostPanel"></div>
|
|
499
|
+
|
|
484
500
|
<div class="kpi" id="kpi"></div>
|
|
485
501
|
|
|
486
502
|
<details id="formulaDetails">
|
|
@@ -583,7 +599,7 @@
|
|
|
583
599
|
<script src="supabase.min.js"></script>
|
|
584
600
|
<script src="fabric.min.js"></script>
|
|
585
601
|
<script src="html2pdf.bundle.min.js"></script>
|
|
586
|
-
<script src="app.js?v=2.
|
|
602
|
+
<script src="app.js?v=2.2.0"></script>
|
|
587
603
|
</body>
|
|
588
604
|
</html>
|
|
589
605
|
|
|
@@ -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 {
|
|
@@ -1788,6 +1821,83 @@
|
|
|
1788
1821
|
font-variant-numeric: tabular-nums;
|
|
1789
1822
|
}
|
|
1790
1823
|
|
|
1824
|
+
.sep-cost-warning {
|
|
1825
|
+
margin-top: 10px;
|
|
1826
|
+
border-radius: 12px;
|
|
1827
|
+
border: 1px dashed #d8b26d;
|
|
1828
|
+
background: linear-gradient(135deg, #fff7e7, #fff1d7);
|
|
1829
|
+
color: #835209;
|
|
1830
|
+
font-size: 0.86rem;
|
|
1831
|
+
font-weight: 700;
|
|
1832
|
+
padding: 10px 12px;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
.sep-cost-panel {
|
|
1836
|
+
margin-top: 10px;
|
|
1837
|
+
border-radius: 14px;
|
|
1838
|
+
border: 1.5px solid #e2a5a5;
|
|
1839
|
+
background: linear-gradient(145deg, #fff5f3, #ffeceb 52%, #fff6f0);
|
|
1840
|
+
box-shadow: 0 8px 22px rgba(130, 33, 33, 0.12);
|
|
1841
|
+
padding: 12px;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
.sep-cost-title {
|
|
1845
|
+
margin: 0 0 8px;
|
|
1846
|
+
font-size: 0.98rem;
|
|
1847
|
+
font-weight: 900;
|
|
1848
|
+
color: #842d2d;
|
|
1849
|
+
letter-spacing: 0.2px;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
.sep-cost-section {
|
|
1853
|
+
display: grid;
|
|
1854
|
+
gap: 7px;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
.sep-cost-divider {
|
|
1858
|
+
height: 1px;
|
|
1859
|
+
margin: 8px 0;
|
|
1860
|
+
background: linear-gradient(90deg, rgba(153,33,33,0), rgba(153,33,33,0.28), rgba(153,33,33,0));
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
.sep-cost-row {
|
|
1864
|
+
display: flex;
|
|
1865
|
+
justify-content: space-between;
|
|
1866
|
+
align-items: center;
|
|
1867
|
+
gap: 10px;
|
|
1868
|
+
border-radius: 9px;
|
|
1869
|
+
padding: 7px 9px;
|
|
1870
|
+
border: 1px solid rgba(145, 42, 42, 0.13);
|
|
1871
|
+
background: rgba(255, 255, 255, 0.72);
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
.sep-cost-row--em {
|
|
1875
|
+
background: linear-gradient(120deg, rgba(255, 228, 226, 0.86), rgba(255, 241, 225, 0.88));
|
|
1876
|
+
border-color: rgba(180, 72, 43, 0.32);
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
.sep-cost-label {
|
|
1880
|
+
font-size: 0.82rem;
|
|
1881
|
+
color: #6a3939;
|
|
1882
|
+
font-weight: 700;
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
.sep-cost-value {
|
|
1886
|
+
font-size: 0.94rem;
|
|
1887
|
+
font-weight: 900;
|
|
1888
|
+
font-variant-numeric: tabular-nums;
|
|
1889
|
+
white-space: nowrap;
|
|
1890
|
+
color: #5d2e2e;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
.sep-loss-negative {
|
|
1894
|
+
color: #9b1f1f;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
.sep-loss-positive {
|
|
1898
|
+
color: #0d7a57;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1791
1901
|
@page {
|
|
1792
1902
|
size: A4 landscape;
|
|
1793
1903
|
margin: 8mm;
|
|
@@ -2209,7 +2319,6 @@
|
|
|
2209
2319
|
box-shadow: 0 12px 26px rgba(17, 70, 64, 0.16);
|
|
2210
2320
|
margin-bottom: 10px;
|
|
2211
2321
|
position: relative;
|
|
2212
|
-
overflow: hidden;
|
|
2213
2322
|
}
|
|
2214
2323
|
|
|
2215
2324
|
.result-pill::after {
|