Private to you. Sam cannot see this unless you ask.
Income by Stream
Income Trend
This Month's Extras
One offs this monthNone
Paid invoices and one off income. Not counted in monthly residual. Resets next month.
Recent Entries
Accounts
Track every account balance against its target
Update Balances
Income Streams
All money coming in
All Income Streams
Expenses
All money going out
All Expenses
Invoices
Create invoices and download as PDF
All Invoices
Goals
Where you are going
All Goals
Tax Estimate
Income tax estimate. Not financial advice.
Tax Breakdown
Your Tax Rate
Enter your effective tax rate to get an accurate estimate. Check with your local tax authority or accountant. NZ rates range from around 17.5% to 33% depending on income.
This is an estimate only. Not financial advice. Always verify with a qualified accountant.
`).join('');
bindMoneyInputs(document.getElementById('accInputs'));
}
function updateBal(id,v){const a=S.accounts.find(a=>a.id===id);if(a){a.balance=parseMoney(v);save();toast('Balance updated','good');}}
function renderIncomeTbl(){document.getElementById('incomeTbl').innerHTML=`
Name
Amount
Freq
Category
Status
Notes
${S.income.map(i=>`
${esc(i.name)}
+${fmtIn(i.amount,i.currency)}
${i.freq==='one-off'?'One off':esc(i.freq||'')}
${esc(i.cat||'')}
${esc(i.status)}
${esc(i.notes||'')}
`).join('')}`;}
function renderExpTbl(){document.getElementById('expTbl').innerHTML=`
`
};
const M_TITLES={income:'Add Income',expense:'Add Expense',account:'Add Account',invoice:'New Invoice',goal:'Add Goal',subscription:'Add Subscription'};
function g(id){const el=document.getElementById(id);return el?el.value.trim():'';}
function n(id){const el=document.getElementById(id);return el?parseMoney(el.value):0;}
function sv(id,val){const el=document.getElementById(id);if(el)el.value=val;}
function svm(id,val){const el=document.getElementById(id);if(el)el.value=fmtMoneyInput(val);}
function openModal(type){
mType=type;mEditId=null;
document.getElementById('modalTitle').textContent=M_TITLES[type]||'Add Entry';
document.getElementById('modalBody').innerHTML=FIELDS[type]();
sv('mCur',getCurCode());
if(type==='invoice'){
const nums=(S.invoices||[]).map(i=>{const m=String(i.invNum||'').match(/(\d+)\s*$/);return m?parseInt(m,10):0;});
const next=(nums.length?Math.max(...nums):0)+1;
sv('mInvNum','INV '+String(next).padStart(3,'0'));
sv('mDate',new Date().toISOString().slice(0,10));
}
bindMoneyInputs(document.getElementById('modalBody'));
document.getElementById('modalOv').classList.add('open');
}
function keyFor(type){return {income:'income',expense:'expenses',account:'accounts',invoice:'invoices',goal:'goals',subscription:'subscriptions'}[type];}
function openEditModal(type,id){
const item=(S[keyFor(type)]||[]).find(x=>x.id===id);
if(!item)return;
mType=type;mEditId=id;
document.getElementById('modalTitle').textContent='Edit '+(M_TITLES[type]||'Entry').replace(/^(Add|New) /,'');
document.getElementById('modalBody').innerHTML=FIELDS[type]();
sv('mName',item.name||'');
svm('mAmount',item.amount);
sv('mFreq',item.freq||'monthly');
sv('mStatus',item.status||'');
sv('mCat',item.cat||'');
sv('mNotes',item.notes||'');
svm('mBalance',item.balance);
svm('mTarget',item.target);
sv('mType',item.type||'other');
sv('mInvNum',item.invNum||'');
sv('mClient',item.client||'');
sv('mClientEmail',item.clientEmail||'');
sv('mDesc',item.desc||'');
sv('mDate',item.date||'');
sv('mDue',item.due||'');
svm('mCurrent',item.current);
sv('mIsCnt',item.isCnt?'1':'');
sv('mDeadline',item.deadline||'');
sv('mCur',item.currency||'NZD');
bindMoneyInputs(document.getElementById('modalBody'));
document.getElementById('modalOv').classList.add('open');
}
function closeModal(){document.getElementById('modalOv').classList.remove('open');mType='';mEditId=null;}
function saveModal(){
const id=mEditId||generateId();
const isEdit=!!mEditId;
const cur=g('mCur')||getCurCode();
let obj=null;
const prevInc=isEdit&&mType==='income'?(S.income||[]).find(x=>x.id===mEditId):null;
const prevInv=isEdit&&mType==='invoice'?(S.invoices||[]).find(x=>x.id===mEditId):null;
if(mType==='income'){
obj={id,name:g('mName'),amount:n('mAmount'),freq:g('mFreq'),status:g('mStatus')||'active',cat:g('mCat'),notes:g('mNotes'),currency:cur};
if(prevInc&&prevInc.fromInvoice)obj.fromInvoice=prevInc.fromInvoice;
if(prevInc&&prevInc.createdAt)obj.createdAt=prevInc.createdAt;
}
else if(mType==='expense')obj={id,name:g('mName'),amount:n('mAmount'),freq:g('mFreq'),status:g('mStatus')||'active',cat:g('mCat'),notes:g('mNotes'),currency:cur};
else if(mType==='account')obj={id,name:g('mName'),balance:n('mBalance'),target:n('mTarget'),type:g('mType')||'other',currency:cur};
else if(mType==='invoice'){
obj={id,invNum:g('mInvNum'),client:g('mClient'),clientEmail:g('mClientEmail'),desc:g('mDesc'),amount:n('mAmount'),date:g('mDate'),due:g('mDue'),status:g('mStatus')||'unpaid',notes:g('mNotes'),currency:cur};
if(obj.status==='paid')obj.paidAt=(prevInv&&prevInv.paidAt)||new Date().toISOString().slice(0,10);
}
else if(mType==='goal')obj={id,name:g('mName'),current:n('mCurrent'),target:n('mTarget'),isCnt:!!g('mIsCnt'),deadline:g('mDeadline'),currency:cur};
else if(mType==='subscription')obj={id,name:g('mName'),amount:n('mAmount'),freq:g('mFreq'),notes:g('mNotes'),currency:cur};
if(!obj)return;
if(!obj.name&&!obj.invNum&&!obj.client){toast('Please enter a name');return;}
const key=keyFor(mType);
if(!S[key])S[key]=[];
if(isEdit){const i=S[key].findIndex(x=>x.id===mEditId);if(i>-1)S[key][i]=obj;}
else{if(mType==='income'||mType==='expense'){obj.createdAt=new Date().toISOString();S[key].unshift(obj);}else S[key].push(obj);}
if(mType==='invoice')ensurePaidInvoiceIncome(obj);
closeModal();
save();
toast(mType==='invoice'&&obj.status==='paid'?'Invoice saved and added to Recent Entries.':'Saved','good');
}
function deleteEntry(type,id){
if(!confirm('Delete this entry? This cannot be undone.'))return;
if(type==='invoice'){
S.income=(S.income||[]).filter(x=>x.fromInvoice!==id);
}
S[keyFor(type)]=(S[keyFor(type)]||[]).filter(x=>x.id!==id);
save();
toast('Deleted');
}
/* AI */
const AISYS='You are the Finance AI inside Beyond Finance on Journey of Many. You talk like a sharp mate who happens to be great with money, not a corporate assistant. Be warm, direct and specific. Always ground your answers in the live financial data provided, referring to the user\'s actual numbers and account names rather than speaking in generalities. Keep replies to a few short sentences unless they ask for detail. Never introduce yourself, never mention being an AI or a language model, never apologise for limitations and skip all corporate filler like I hope this helps. Encourage real progress without empty flattery and be honest when the numbers need attention. Frame guidance as general information rather than formal financial advice. Use Australian English spelling. Never use hyphens or dashes of any kind in your responses and never place a comma before the word and.';
function needsLiveData(q){
return /(income|expense|spend|spent|balance|tax|invoice|goal|money|net|rate|account|save|budget|profit|gst|dollar|nzd|usd|how much|subscription|cash|owe|paid)/i.test(String(q||''));
}
function getCtx(){
const mi=getMI();
const me=(S.expenses||[]).filter(e=>e.status==='active').reduce((s,e)=>s+monthlyOf(e),0);
const ta=(S.accounts||[]).reduce((s,a)=>s+toBase(a.balance,a.currency),0);
const tgt=getTargetTotal();
let ctx='LIVE FINANCIAL DATA:\n';
ctx+='Display currency: '+getCurCode()+'\n';
ctx+='Monthly income (active streams): '+fmt(mi)+'\n';
ctx+='Monthly expenses: '+fmt(me)+'\n';
ctx+='Net per month: '+fmt(mi-me)+'\n';
ctx+='Total assets: '+fmt(ta)+'\n';
if(tgt>0)ctx+='Total account target: '+fmt(tgt)+' ('+Math.round((ta/tgt)*100)+'% reached)\n';
if(S.income?.length)ctx+='Income streams: '+S.income.map(i=>i.name+' ('+fmtIn(i.amount,i.currency)+' '+i.freq+', '+i.status+')').join('; ')+'\n';
if(S.expenses?.length)ctx+='Expenses: '+S.expenses.map(e=>e.name+' ('+fmtIn(e.amount,e.currency)+' '+e.freq+')').join('; ')+'\n';
if(S.goals?.length)ctx+='Goals: '+S.goals.map(gl=>{const p=gl.target>0?Math.round((gl.current/gl.target)*100):0;return gl.name+' ('+p+'%)';}).join('; ')+'\n';
if(S.subscriptions?.length)ctx+='Subscriptions: '+S.subscriptions.map(x=>x.name+' ('+fmtIn(x.amount,x.currency)+' '+x.freq+')').join('; ')+'\n';
if(S.invoices?.length){const up=S.invoices.filter(v=>v.status==='unpaid').length;ctx+='Invoices: '+S.invoices.length+' total, '+up+' unpaid\n';}
ctx+='The user is a creator managing their finances through Beyond Finance on Journey of Many.\n';
return ctx;
}
function initAI(){
const msgs=document.getElementById('aiMsgs');
if(!msgs)return;
msgs.innerHTML='';
const ta=(S.accounts||[]).reduce((s,a)=>s+toBase(a.balance,a.currency),0);
const mi=getMI();
const fn=firstName();
let welcome=(fn?'Hey '+fn+'! ':'Hey! ')+'I am your Beyond Finance AI. You currently have '+fmt(ta)+' across your accounts and '+fmt(mi)+' coming in monthly from active streams. What would you like to explore?';
addAiMsg('bf',welcome);
showAiSugs();
}
function addAiMsg(who,text){
const msgs=document.getElementById('aiMsgs');
const div=document.createElement('div');
div.className='ai-msg '+who;
const av=who==='bf'?'BF':(getCurrentUser()?.display_name||'U').charAt(0).toUpperCase();
div.innerHTML=`
${av}
${who==='bf'?fmtAI(text):esc(text)}
`;
msgs.appendChild(div);
msgs.scrollTop=msgs.scrollHeight;
}
function fmtAI(t){return esc(t).replace(/\*\*(.+?)\*\*/g,'$1').replace(/\*(.+?)\*/g,'$1').replace(/\n/g,' ');}
function showTyping(){const msgs=document.getElementById('aiMsgs');const div=document.createElement('div');div.className='ai-msg bf';div.id='aiTyping';div.innerHTML='
BF
';msgs.appendChild(div);msgs.scrollTop=msgs.scrollHeight;}
function hideTyping(){document.getElementById('aiTyping')?.remove();}
function showAiSugs(){
const el=document.getElementById('aiSugs');
if(!el)return;
const sugs=['Am I on track for my goals','How can I grow my income','Where should I focus this month','What is my biggest financial risk','How do my expenses look','Estimate my tax for this year'];
el.innerHTML=sugs.map(s=>``).join('');
}
function quickAI(q){document.getElementById('aiInp').value=q;sendAI();}
function clearAI(){S.aiHistory=[];save();initAI();}
async function sendAI(){
if(aiTyping)return;
const inp=document.getElementById('aiInp');
const btn=document.getElementById('aiBtn');
const q=inp.value.trim();
if(!q)return;
inp.value='';autoResize(inp);
addAiMsg('user',q);
aiTyping=true;btn.disabled=true;
showTyping();
if(!S.aiHistory)S.aiHistory=[];
S.aiHistory.push({role:'user',content:q});
if(S.aiHistory.length>20)S.aiHistory=S.aiHistory.slice(-20);
try{
const live=needsLiveData(q);
const r=await apiCall('/chat','POST',{system:AISYS+'\n\n'+(live?getCtx():'The user has a Beyond Finance dashboard. Do not quote balances, income, expenses or account names unless they ask about their numbers.'),messages:S.aiHistory});
const reply=r.reply||r.message||r.content||'Sorry, I could not come up with a reply. Please try again.';
S.aiHistory.push({role:'assistant',content:reply});
hideTyping();
addAiMsg('bf',reply);
save();
}catch(e){
hideTyping();
addAiMsg('bf','Could not connect right now. Please try again in a moment.');
}
aiTyping=false;btn.disabled=false;
}
function aiKey(e){if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendAI();}}
function autoResize(el){el.style.height='auto';el.style.height=Math.min(el.scrollHeight,100)+'px';}
/* EXPORT */
function csvCell(v){const s=String(v??'');return /[",\n]/.test(s)?'"'+s.replace(/"/g,'""')+'"':s;}
function exportCSV(){
const rows=[];
const add=r=>rows.push(r.map(csvCell).join(','));
add(['BEYOND FINANCE BACKUP',new Date().toLocaleDateString('en-NZ')]);
add([]);
add(['ACCOUNTS']);
add(['Name','Type','Balance','Currency','Target']);
(S.accounts||[]).forEach(a=>add([a.name,a.type||'',a.balance,a.currency||'NZD',a.target||'']));
add([]);
add(['INCOME']);
add(['Name','Category','Amount','Currency','Frequency','Status','Notes']);
(S.income||[]).forEach(i=>add([i.name,i.cat||'',i.amount,i.currency||'NZD',i.freq||'',i.status||'',i.notes||'']));
add([]);
add(['EXPENSES']);
add(['Name','Category','Amount','Currency','Frequency','Status','Notes']);
(S.expenses||[]).forEach(e=>add([e.name,e.cat||'',e.amount,e.currency||'NZD',e.freq||'',e.status||'',e.notes||'']));
add([]);
add(['INVOICES']);
add(['Number','Client','Client Email','Description','Amount','Currency','Date','Due','Status','Notes']);
(S.invoices||[]).forEach(i=>add([i.invNum||'',i.client||'',i.clientEmail||'',i.desc||'',i.amount,i.currency||'NZD',i.date||'',i.due||'',i.status||'',i.notes||'']));
add([]);
add(['SUBSCRIPTIONS']);
add(['Name','Amount','Currency','Frequency','Notes']);
(S.subscriptions||[]).forEach(s2=>add([s2.name,s2.amount,s2.currency||'NZD',s2.freq||'',s2.notes||'']));
add([]);
add(['GOALS']);
add(['Name','Current','Target','Currency','Deadline']);
(S.goals||[]).forEach(g2=>add([g2.name,g2.current||'',g2.target||'',g2.currency||'NZD',g2.deadline||'']));
const blob=new Blob(['\uFEFF'+rows.join('\n')],{type:'text/csv;charset=utf-8'});
const a=document.createElement('a');
a.href=URL.createObjectURL(blob);
a.download='beyond finance backup '+new Date().toISOString().slice(0,10)+'.csv';
document.body.appendChild(a);a.click();a.remove();
toast('CSV backup downloaded','good');
}
let _pdfLoad=null;
function ensureHtml2Pdf(){
if(typeof html2pdf!=='undefined')return Promise.resolve();
if(_pdfLoad)return _pdfLoad;
_pdfLoad=new Promise(function(resolve,reject){
const s=document.createElement('script');
s.src='https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js';
s.async=true;
s.onload=function(){resolve();};
s.onerror=function(){_pdfLoad=null;reject(new Error('PDF library failed to load'));};
document.head.appendChild(s);
});
return _pdfLoad;
}
function pdfRow(label,value){
return '
'+label+'
'+value+'
';
}
function pdfSection(title,rows){
if(!rows)return '';
return '
'+title+'
'+rows+'
';
}
function buildSummaryReportHtml(){
const st=S.settings||{};
const mi=getMI();
const me=(S.expenses||[]).filter(e=>e.status==='active').reduce((s,e)=>s+monthlyOf(e),0);
const extras=getExtrasTotal();
const made=getMadeThisMonth();
const ta=(S.accounts||[]).reduce((s,a)=>s+toBase(a.balance,a.currency),0);
const tgt=getTargetTotal();
let rows=pdfRow('Monthly residual',fmtFullDisp(mi))+pdfRow('Extras this month',fmtFullDisp(extras))+pdfRow('Made this month',fmtFullDisp(made))+pdfRow('Monthly expenses',fmtFullDisp(me))+pdfRow('Net residual',fmtFullDisp(mi-me))+pdfRow('Total Assets',fmtFullDisp(ta));
if(tgt>0)rows+=pdfRow('Target Progress',fmtFullDisp(ta)+' of '+fmtFullDisp(tgt)+' ('+Math.round((ta/tgt)*100)+'%)');
let accRows='';
(S.accounts||[]).forEach(a=>{accRows+=pdfRow(esc(a.name)+' ('+(a.currency||'NZD')+')',fmtFull(a.balance,a.currency||'NZD'));});
let goalRows='';
(S.goals||[]).forEach(gl=>{
const p=gl.target>0?Math.round((gl.current/gl.target)*100):0;
const gcur=gl.currency||getCurCode();
const cf=gl.isCnt?(gl.current+' of '+gl.target):(fmtFull(gl.current,gcur)+' of '+fmtFull(gl.target,gcur)+' '+gcur);
goalRows+=pdfRow(esc(gl.name),cf+' ('+p+'%)');
});
return '
'+
'
'+esc(st.bizName||'Beyond Finance')+'
'+
'
Financial summary generated '+new Date().toLocaleDateString('en-NZ')+' \u00B7 Displayed in '+getCurCode()+'
This is your private money picture. Sam cannot see it unless you tap Share with Sam or Get Sam's help. You need to fill in your business, accounts, income, expenses and a goal before the dashboard opens. There is no skip.
Add at least one goal with a name and a target amount.
${obPrivacy()}
${OB.goals.map((a,i)=>obGoalItem(a,i)).join('')}
${obFoot()}`;
}else{
el.innerHTML=`
You are set.
Your dashboard is ready and still private. Share with Sam or Get Sam's help only when you want him to see it.
`;
}
bindMoneyInputs(el);
}
function obAccountItem(a,i){return `
`;}
function obIncomeItem(a,i){return `
`;}
function obExpenseItem(a,i){return `
`;}
function obGoalItem(a,i){return `
`;}
function addOBItem(key){
if(key==='accounts')OB.accounts.push({name:'',balance:0,target:0,currency:OB.biz.currency});
if(key==='income')OB.income.push({name:'',amount:0,freq:'monthly',status:'active'});
if(key==='expenses')OB.expenses.push({name:'',amount:0,freq:'monthly'});
if(key==='goals')OB.goals.push({name:'',current:0,target:0,deadline:''});
renderOnboardStep();
}
function removeOBItem(key,i){
if((OB[key]||[]).length<=1){toast('You need at least one');return;}
OB[key].splice(i,1);
renderOnboardStep();
}
function captureOBStep(){
if(obKey()==='biz'){
const n=document.getElementById('obBizName');
if(!n)return;
OB.biz.bizName=n.value.trim();
OB.biz.email=document.getElementById('obEmail').value.trim();
OB.biz.country=document.getElementById('obCountry').value.trim();
OB.biz.currency=document.getElementById('obCurrency').value;
}
}
function nextOBStep(){
captureOBStep();
const key=obKey();
if(key==='biz'){
if(!OB.biz.bizName||!OB.biz.bizName.trim()){toast('Add your business or brand name');return;}
if(!OB.biz.email||!OB.biz.email.trim()){toast('Add your email');return;}
if(!OB.biz.country||!OB.biz.country.trim()){toast('Add your country');return;}
}
if(key==='accounts'){
const ok=OB.accounts.some(a=>a.name&&a.name.trim());
if(!ok){toast('Add at least one account name');return;}
}
if(key==='income'){
const ok=OB.income.some(a=>a.name&&a.name.trim()&&Number(a.amount)>0);
if(!ok){toast('Add at least one income stream with a name and amount');return;}
}
if(key==='expenses'){
const ok=OB.expenses.some(a=>a.name&&a.name.trim()&&Number(a.amount)>0);
if(!ok){toast('Add at least one expense with a name and amount');return;}
}
if(key==='goals'){
const ok=OB.goals.some(g=>g.name&&g.name.trim()&&Number(g.target)>0);
if(!ok){toast('Add at least one goal with a name and target amount');return;}
}
const flow=obFlow();
if(obStep0){obStep--;renderOnboardStep();}
}
function applyOnboardingData(){
const cur=OB.biz.currency||'NZD';
if(!S.settings)S.settings={};
if(OB.biz.bizName)S.settings.bizName=OB.biz.bizName;
if(OB.biz.email)S.settings.email=OB.biz.email;
if(OB.biz.country)S.settings.country=OB.biz.country;
S.settings.defaultCurrency=cur;
S.settings.dashMode='full';
if(!S.curManual)S.currency=cur;
OB.accounts.filter(a=>a.name).forEach(a=>{S.accounts.push({id:generateId(),name:a.name,balance:a.balance||0,target:a.target||0,type:'other',currency:a.currency||cur});});
OB.income.filter(i=>i.name).forEach(i=>{S.income.push({id:generateId(),name:i.name,amount:i.amount||0,freq:i.freq||'monthly',status:i.status||'active',cat:'',notes:'',currency:cur});});
OB.expenses.filter(e=>e.name).forEach(e=>{S.expenses.push({id:generateId(),name:e.name,amount:e.amount||0,freq:e.freq||'monthly',status:'active',cat:'',notes:'',currency:cur});});
OB.goals.filter(gl=>gl.name&&Number(gl.target)>0).forEach(gl=>{S.goals.push({id:generateId(),name:gl.name,current:gl.current||0,target:gl.target||0,isCnt:false,deadline:gl.deadline||'',currency:cur});});
S.onboarded=true;
if(!S.samShare)S.samShare={on:false,kind:'',at:0,note:''};
applySettings();
save();
toast('Dashboard set up. It stays private until you ask Sam.','good');
}
function finishOnboarding(){
applyOnboardingData();
const screen=document.getElementById('onboardScreen');
screen.classList.remove('open');
screen.style.display='none';
document.getElementById('userMenu').style.display='flex';
renderAll();
}
function samShareOn(){return !!(S.samShare&&S.samShare.on);}
function renderSamBar(){
const on=samShareOn();
const html=on
? `
Shared with Sam. Only Sam can see this, not the public site.
`
: ``;
const a=document.getElementById('samActions');
const b=document.getElementById('samActionsSettings');
if(a)a.innerHTML=html;
if(b)b.innerHTML=html;
const p=document.getElementById('dashPrivate');
if(p)p.textContent=on?'Shared with Sam. Still hidden from the public site.':'Private to you. Sam cannot see this unless you ask.';
}
async function shareWithSam(kind){
S.samShare={on:true,kind:kind||'share',at:Date.now(),note:''};
try{await apiCall('/data/save','POST',{key:'sam-share',value:S.samShare});}catch(e){}
save();
toast(kind==='help'?'Sam will get your ask for help. Your dashboard stays off the public site.':'Shared with Sam. Only Sam can see this.','good');
renderSamBar();
}
async function stopSamShare(){
S.samShare={on:false,kind:'',at:Date.now(),note:''};
try{await apiCall('/data/save','POST',{key:'sam-share',value:S.samShare});}catch(e){}
save();
toast('Sharing stopped. Your dashboard is private again.','good');
renderSamBar();
}
/* PWA */
let deferredPrompt=null;
window.addEventListener('beforeinstallprompt',e=>{e.preventDefault();deferredPrompt=e;const b=document.getElementById('installBtn');if(b)b.style.display='block';});
async function installPWA(){
if(!deferredPrompt){toast('On iPhone tap Share then Add to Home Screen');return;}
deferredPrompt.prompt();
await deferredPrompt.userChoice;
deferredPrompt=null;
const b=document.getElementById('installBtn');if(b)b.style.display='none';
}
function setupPwaManifest(){
try{
const el=document.getElementById('pwaManifest');
if(!el)return;
if(lastManifestUrl){URL.revokeObjectURL(lastManifestUrl);lastManifestUrl=null;}
const manifest={
name:'Beyond Finance',short_name:'Beyond Finance',description:'Your personal finance dashboard',
start_url:window.location.href,display:'standalone',background_color:'#070708',theme_color:'#cf1322',
orientation:'portrait-primary',
icons:[
{src:window.location.origin+'/bf-app-192.png',sizes:'192x192',type:'image/png'},
{src:window.location.origin+'/bf-app-512.png',sizes:'512x512',type:'image/png'}
]
};
lastManifestUrl=URL.createObjectURL(new Blob([JSON.stringify(manifest)],{type:'application/json'}));
el.href=lastManifestUrl;
}catch(e){}
}
/* INIT */
function appHomeUrl(){
try{
if((location.hostname||'').indexOf('journeyofmany.com')!==-1) return location.origin+location.pathname;
}catch(e){}
return 'https://www.journeyofmany.com/beyond-finance-app/';
}
function accountSignInUrl(){
return 'https://www.journeyofmany.com/account/?next='+encodeURIComponent(appHomeUrl());
}
async function initApp(){
try{
setupPwaManifest();
injectIcons();
const now=new Date();
const td=document.getElementById('topDate');
if(td)td.textContent=now.toLocaleDateString('en-NZ',{weekday:'short',day:'numeric',month:'short',year:'numeric'});
refreshRates();
const idle=window.requestIdleCallback||function(cb){return setTimeout(cb,1200);};
idle(function(){if('serviceWorker' in navigator)navigator.serviceWorker.register('/sw.js').catch(function(){});});
S=JSON.parse(JSON.stringify(DEF));
const accountHref=accountSignInUrl();
if(parseResetToken()){
document.getElementById('loginScreen').style.display='flex';
showForgot();
return;
}
if(getToken()){
const ok=await refreshUser();
if(!ok){
if((location.hostname||'').indexOf('journeyofmany.com')!==-1){ window.location.replace(accountHref); return; }
document.getElementById('loginScreen').style.display='flex';
return;
}
await loadServerData();
captureUtms();
enterDash(getCurrentUser(),!S.onboarded);
}else{
if((location.hostname||'').indexOf('journeyofmany.com')!==-1){
window.location.replace(accountHref);
} else {
document.getElementById('loginScreen').style.display='flex';
}
}
}catch(e){
document.getElementById('loginScreen').style.display='flex';
}
}
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',initApp);else initApp();
document.addEventListener('click',e=>{if(!e.target.closest('.user-menu'))closeDD();if(!e.target.closest('.notif-wrap'))closeNotifPanel();});
document.getElementById('logoutConfirm')?.addEventListener('click',confirmLogout);
document.getElementById('logoutCancel')?.addEventListener('click',()=>setLogoutOpen(false));
document.getElementById('logoutPanel')?.addEventListener('click',e=>{if(e.target.id==='logoutPanel')setLogoutOpen(false);});
document.addEventListener('keydown',e=>{if(e.key!=='Escape')return; if(document.getElementById('logoutPanel')?.classList.contains('is-on')){setLogoutOpen(false);return;} if(document.getElementById('mobileNav')?.classList.contains('open')) closeMobileNav();});
document.getElementById('modalOv').addEventListener('click',e=>{if(e.target===document.getElementById('modalOv'))closeModal();});
document.addEventListener('keydown',e=>{const ov=document.getElementById('modalOv');if(!ov.classList.contains('open'))return;if(e.key==='Escape'){e.preventDefault();closeModal();}if(e.key==='Enter'&&e.target.tagName!=='TEXTAREA'){e.preventDefault();saveModal();}});