ps: auto-derive years of service from join year + retirement age
This commit is contained in:
1 parent
a0fbb390b5
commit
97cecb91a7
17 files changed
+667
-18
No files matched your search
+66
-6
@@ -51,7 +51,7 @@
|
||||
retireAge: 65, spendingAnnual: 50000,
|
||||
cppStartAge: 65, oasStartAge: 65,
|
||||
pensionAnnual: 0, rentalAnnual: 0, otherAnnual: 0,
|
||||
psPension: false, psJoinYear: null, psServiceYears: null, psAvgSalary: null,
|
||||
psPension: false, psJoinYear: null, psServiceYears: null, psServiceOverride: null, psAvgSalary: null,
|
||||
psOccupation: null, psItLevel: null, psItStep: 0,
|
||||
realReturnPre: 3.5, realReturnPost: 2.5, inflation: 2.0, lifeExp: 95,
|
||||
interests: []
|
||||
@@ -65,6 +65,11 @@
|
||||
if (m) { try { raw = decodeURIComponent(escape(atob(m[1]))); } catch (e) { raw = null; } }
|
||||
if (!raw) { try { raw = localStorage.getItem(LS); } catch (e) { raw = null; } }
|
||||
if (raw) { try { var o = JSON.parse(raw); var b = Object.assign({}, o.a || o); delete b.lang; Object.assign(state.a, b); state.step = o.step | 0; if (o.screen) state.screen = o.screen; else if (m) state.screen = 'results'; } catch (e) { /* fresh */ } }
|
||||
/* Migration (PS-AUTO-SERVICE): legacy saved psServiceYears was always user-typed →
|
||||
treat it as the override so pre-auto-derivation plans compute exactly as before. */
|
||||
if (state.a.psServiceOverride == null && typeof state.a.psServiceYears === 'number' && isFinite(state.a.psServiceYears)) {
|
||||
state.a.psServiceOverride = state.a.psServiceYears;
|
||||
}
|
||||
})();
|
||||
function save() {
|
||||
var t2 = { a: state.a, step: state.step, screen: state.screen, lang: getLang() };
|
||||
@@ -106,6 +111,24 @@
|
||||
return p;
|
||||
}
|
||||
function psOn() { return !!state.a.psPension; }
|
||||
/* Auto-derived pensionable service (PS-AUTO-SERVICE): retirement age − age at join,
|
||||
from current age + join year. Null when inputs are missing/invalid. */
|
||||
var PS_CUR_YEAR = new Date().getFullYear();
|
||||
function psAutoServiceYears() {
|
||||
if (typeof PS === 'undefined' || !PS.autoServiceYears) return null;
|
||||
return PS.autoServiceYears(state.a.age, state.a.psJoinYear, state.a.retireAge, PS_CUR_YEAR);
|
||||
}
|
||||
/* EFFECTIVE service years — the single contract the engine + summary lines must use:
|
||||
user override when set, else the auto derivation (null = not derivable yet). */
|
||||
function psServiceEffective() {
|
||||
var o = state.a.psServiceOverride;
|
||||
if (o != null && isFinite(+o)) return +o;
|
||||
return psAutoServiceYears();
|
||||
}
|
||||
function psOverrideActive() {
|
||||
var o = state.a.psServiceOverride;
|
||||
return o != null && isFinite(+o) && psAutoServiceYears() != null;
|
||||
}
|
||||
function psAugment(p) { /* re-derive PS pension for a scenario's retireAge */
|
||||
var ps = psCompute(p.retireAge);
|
||||
if (ps) { p.pensionAnnual = ps.pensionAtRetirement; p.pensionStartAge = ps.startAge; p.pensionBridgeAnnual = ps.bridge; }
|
||||
@@ -118,7 +141,9 @@
|
||||
if (!avg) return null;
|
||||
return PS.computePSPension({
|
||||
joinYear: +a.psJoinYear || 0, retireAge: retireAge,
|
||||
serviceYears: +a.psServiceYears || 0, avgSalary: avg,
|
||||
/* EFFECTIVE value: override when set, else auto-derivation (never null while
|
||||
the module is computable — PS-AUTO-SERVICE). */
|
||||
serviceYears: +psServiceEffective() || 0, avgSalary: avg,
|
||||
ampe: (typeof CANADA !== 'undefined' && CANADA.ympe && CANADA.ympe.value) || 74600
|
||||
});
|
||||
}
|
||||
@@ -215,8 +240,24 @@
|
||||
get: function () { return state.a.psJoinYear; }, set: function (v) { state.a.psJoinYear = +v; }
|
||||
},
|
||||
{
|
||||
key: 'ps_service', show: psOn, titleK: 'q.ps_service.title', hintK: 'q.ps_service.hint', type: 'number', min: 0, max: 45,
|
||||
get: function () { return state.a.psServiceYears; }, set: function (v) { state.a.psServiceYears = +v; }
|
||||
key: 'ps_service', show: psOn, titleK: 'q.ps_service.title',
|
||||
hint: function () {
|
||||
var auto = psAutoServiceYears(), o = state.a.psServiceOverride;
|
||||
if (o != null && isFinite(+o)) {
|
||||
return t('q.ps_service.override', { years: fmtInt(+o), auto: auto == null ? '—' : fmtInt(auto) });
|
||||
}
|
||||
if (auto != null) {
|
||||
return t('q.ps_service.auto', {
|
||||
join: state.a.psJoinYear,
|
||||
joinAge: fmtInt(+state.a.age - (PS_CUR_YEAR - +state.a.psJoinYear)),
|
||||
ret: state.a.retireAge, years: fmtInt(auto)
|
||||
});
|
||||
}
|
||||
return t('q.ps_service.hint');
|
||||
},
|
||||
type: 'number', min: 0, max: 60, /* auto-derivation clamps to 60 (engine caps 35) */
|
||||
get: function () { var e = psServiceEffective(); return e == null ? '' : e; },
|
||||
set: function (v) { state.a.psServiceOverride = (v === '' || v == null || isNaN(+v)) ? null : +v; }
|
||||
},
|
||||
{
|
||||
key: 'ps_salary', show: psOn, titleK: 'q.ps_salary.title',
|
||||
@@ -314,9 +355,10 @@
|
||||
btnNext.textContent = state.step === vs.length - 1 ? t('quiz.seePlan') : t('quiz.next');
|
||||
|
||||
var hintTxt = typeof s.hint === 'function' ? s.hint() : (s.hintK ? t(s.hintK) : '');
|
||||
var hintAttr = s.key === 'ps_service' ? ' id="ps-hint"' : ''; /* PS-AUTO-SERVICE: live-swap hook */
|
||||
var titleTxt = typeof s.titleK === 'function' ? s.titleK() : t(s.titleK);
|
||||
var optList = typeof s.opts === 'function' ? s.opts() : s.opts;
|
||||
var h = '<h2>' + titleTxt + '</h2>' + (hintTxt ? '<p class="hint">' + hintTxt + '</p>' : '');
|
||||
var h = '<h2>' + titleTxt + '</h2>' + (hintTxt ? '<p class="hint"' + hintAttr + '>' + hintTxt + '</p>' : '');
|
||||
|
||||
if (s.type === 'grid') {
|
||||
h += '<div class="opts">' + optList.map(function (o) {
|
||||
@@ -334,6 +376,12 @@
|
||||
}).join('') + '</div>';
|
||||
} else if (s.type === 'number') {
|
||||
h += '<div class="fld"><input type="number" id="f1" value="' + (s.get() == null ? '' : s.get()) + '" min="' + (s.min != null ? s.min : 0) + '" max="' + (s.max != null ? s.max : 1e9) + '" inputmode="numeric" ' + (s.money ? 'placeholder="0"' : 'placeholder="' + fmtInt(s.max) + '"') + '></div>';
|
||||
/* PS-AUTO-SERVICE: when the user has overridden the auto-derived service, offer the
|
||||
CPP-"Use maximum"-style reset back to the calculated value. Always in the DOM so
|
||||
oninput can reveal/hide it live (real browsers only re-render on step change). */
|
||||
if (s.key === 'ps_service') {
|
||||
h += '<p class="hint" id="ps-svc-reset" style="margin:8px 0 0' + (psOverrideActive() ? '' : ';display:none') + '"><button type="button" class="link" id="ps-usecalc">' + t('q.ps_service.useCalc') + '</button></p>';
|
||||
}
|
||||
} else if (s.type === 'fields') {
|
||||
h += s.fields.map(function (f, i) {
|
||||
if (f.key === 'pensionAnnual' && psOn()) {
|
||||
@@ -369,11 +417,23 @@
|
||||
});
|
||||
var f1 = $('#f1');
|
||||
if (f1) {
|
||||
f1.oninput = function () { s.set(f1.value); save(); liveUpdate(); validate(s); if (s.type === 'slider') $('#sv').textContent = s.fmt(+f1.value); };
|
||||
f1.oninput = function () {
|
||||
s.set(f1.value); save(); liveUpdate(); validate(s);
|
||||
if (s.type === 'slider') $('#sv').textContent = s.fmt(+f1.value);
|
||||
/* PS-AUTO-SERVICE: real browsers keep the rendered hint on typing (no re-render until
|
||||
step change) — swap the derivation hint + reveal the reset button live. */
|
||||
if (s.key === 'ps_service') {
|
||||
var he = $('#ps-hint'); if (he) he.textContent = s.hint();
|
||||
var rb = $('#ps-svc-reset'); if (rb) rb.style.display = psOverrideActive() ? '' : 'none';
|
||||
}
|
||||
};
|
||||
if (s.type === 'slider') f1.oninput();
|
||||
/* Fix 8: Enter on a number step = press Next (when enabled). */
|
||||
if (s.type === 'number') f1.onkeydown = function (e) { if (e.key === 'Enter' && !btnNext.disabled) btnNext.click(); };
|
||||
}
|
||||
/* PS-AUTO-SERVICE: "Use calculated" clears the override → auto value flows to engine + summary. */
|
||||
var uc = $('#ps-usecalc');
|
||||
if (uc) uc.onclick = function () { state.a.psServiceOverride = null; save(); liveUpdate(); renderStep(); };
|
||||
stepBody.querySelectorAll('input[type=number][data-k]').forEach(function (inp) {
|
||||
inp.oninput = function () { s.set(inp.dataset.k, inp.value); save(); liveUpdate(); validate(s); };
|
||||
});
|
||||
|
||||
@@ -87,6 +87,25 @@
|
||||
return y <= GROUP1_CUTOFF_YEAR ? 1 : 2;
|
||||
}
|
||||
|
||||
/* Auto-derive years of pensionable service (PS-AUTO-SERVICE):
|
||||
* yearsWorkedSoFar = currentYear − joinYear
|
||||
* ageAtJoin = age − yearsWorkedSoFar
|
||||
* serviceYears = retireAge − ageAtJoin, clamped to [0, 60] (whole years;
|
||||
* the pension formula applies its own 35-yr MAX_SERVICE cap).
|
||||
* currentYear defaults to the calendar year. Returns null when age, joinYear or
|
||||
* retireAge is missing/invalid — the UI then falls back to manual entry. */
|
||||
function autoServiceYears(age, joinYear, retireAge, currentYear) {
|
||||
var y = +currentYear;
|
||||
if (!(y > 0)) y = new Date().getFullYear();
|
||||
var a = +age, j = +joinYear, r = +retireAge;
|
||||
if (!(a > 0) || !(r > 0) || !(y > 0)) return null;
|
||||
if (!(j >= 1900 && j <= 2999)) return null;
|
||||
if (!isFinite(a) || !isFinite(j) || !isFinite(r) || !isFinite(y)) return null;
|
||||
var joinAge = a - (y - j);
|
||||
var s = r - joinAge;
|
||||
return Math.round(Math.max(0, Math.min(60, s)));
|
||||
}
|
||||
|
||||
function itLevel(id) {
|
||||
for (var i = 0; i < IT_LEVELS.length; i++) if (IT_LEVELS[i].id === id) return IT_LEVELS[i];
|
||||
return null;
|
||||
@@ -194,7 +213,7 @@
|
||||
SOURCES: SOURCES,
|
||||
psGroup: psGroup, psLifetimePension: psLifetimePension,
|
||||
psBridgeBenefit: psBridgeBenefit, psEarlyReduction: psEarlyReduction,
|
||||
computePSPension: computePSPension,
|
||||
computePSPension: computePSPension, autoServiceYears: autoServiceYears,
|
||||
itLevel: itLevel, itRange: itRange
|
||||
};
|
||||
});
|
||||
+8
-2
@@ -122,7 +122,10 @@
|
||||
},
|
||||
ps_service: {
|
||||
title: 'Years of pensionable service',
|
||||
hint: 'Recognized by PSPC (the calculation caps it at 35 years).'
|
||||
hint: 'Recognized by PSPC (the calculation caps it at 35 years).',
|
||||
auto: 'Calculated: joined {join} at age {joinAge}, retiring at {ret} → {years} years of service. Adjust only if you bought past service.',
|
||||
override: 'Using your value ({years} years). The calculation from your join year gives {auto} years.',
|
||||
useCalc: 'Use calculated'
|
||||
},
|
||||
ps_salary: {
|
||||
title: 'Average salary — your 5 consecutive highest-paid years',
|
||||
@@ -379,7 +382,10 @@
|
||||
},
|
||||
ps_service: {
|
||||
title: 'Années de service ouvrant droit à pension',
|
||||
hint: 'Reconnues par la PSCC (le calcul est plafonné à 35 années).'
|
||||
hint: 'Reconnues par la PSCC (le calcul est plafonné à 35 années).',
|
||||
auto: 'Calculé : entrée en poste en {join} à {joinAge} ans, départ à {ret} ans → {years} années de service. À ajuster seulement si vous avez acheté des services antérieurs.',
|
||||
override: 'Votre valeur ({years} années) est utilisée. Le calcul à partir de votre année d\'entrée donne {auto} ans.',
|
||||
useCalc: 'Utiliser le calcul'
|
||||
},
|
||||
ps_salary: {
|
||||
title: 'Salaire moyen — vos 5 années consécutives les mieux rémunérées',
|
||||
|
||||
@@ -104,6 +104,7 @@ button,input{touch-action:manipulation}
|
||||
#step-body{background:var(--card);border:1px solid var(--line);border-radius:var(--r);padding:18px;min-height:320px}
|
||||
#step-body h2{margin-top:.1em}
|
||||
.hint{font-size:13.5px;color:var(--ink2)}
|
||||
.link{background:none;border:0;padding:0;font:inherit;font-weight:700;color:var(--blue);cursor:pointer;text-decoration:underline;text-underline-offset:2px} /* text-button (e.g. "Use calculated" reset on the auto-derived service step) */
|
||||
.fld{margin:14px 0}
|
||||
.fld label{display:block;font-weight:600;font-size:15px;margin-bottom:5px}
|
||||
.fld .unit{font-size:13px;color:var(--ink2)}
|
||||
|
||||
Reference in new issue
Block a user