Dashboard
My properties
Alerts & reminders
LVR tracker
● <80% safe ● 80–90% watch ● >90% high
Monthly statements
Capital expenses
Depreciation schedule
Cashflow
Tax & reports
Documents
Settlement documents
Contracts, section 32s, title deeds
Leasing
Insurance
Equity growth timeline
Year-on-year comparison
CGT calculator
Import CSV
Archived
Share access
Yield calculator
Refinance tracker
' +(depDocs.length?depDocs.map(d=>'
'+esc(d.title||d.name)+(d.serverUrl||d.cloudUrl?'synced':'pending')+'
'+esc(d.type||'')+' · '+esc(d.date||'')+'
').join('') :'
No reports uploaded. Upload your quantity surveyor depreciation schedule here.
') +''; }).join(''); el.innerHTML = '
Enter your annual depreciation figures from your quantity surveyor report. Div 43 covers building structure (2.5%/yr), Div 40 covers plant and equipment items.
' +'
' +'
Total deduction
'+f(totalDiv43+totalDiv40)+'
'+fy+'
' +'
Div 43 structural
'+f(totalDiv43)+'
' +'
Div 40 plant & equip
'+f(totalDiv40)+'
' +'
'+cards; } // ============================================================= // CASHFLOW // ============================================================= function populateCfMonthSelect() { const el = document.getElementById('cf-month-sel'); if (!el) return; const months = []; for (let y = 2023; y <= 2027; y++) for (let m = 1; m <= 12; m++) months.push(`${y}-${String(m).padStart(2,'0')}`); const cur = el.value || new Date().toISOString().slice(0,7); el.innerHTML = months.map(m => `${m}`).join(''); } function populateCfFYSelect() { const el = document.getElementById('cf-fy-sel'); if (!el) return; const fys = allFYs(); const cur = currentFY(); el.innerHTML = fys.reverse().map(fy => `${fy}`).join(''); } function setCfPeriod(p, el) { CF_PERIOD = p; document.querySelectorAll('#cf-pill-mo,#cf-pill-yr').forEach(x=>x.classList.remove('active')); el.classList.add('active'); const monthSel = document.getElementById('cf-month-sel'); const fySel = document.getElementById('cf-fy-sel'); if (monthSel) monthSel.style.display = p === 'mo' ? '' : 'none'; if (fySel) fySel.style.display = p === 'yr' ? '' : 'none'; renderCashflow(); } function renderCashflow() { const el = document.getElementById('cf-content'); if (!el) return; if (!PS.length) { el.innerHTML = emptyProps(); return; } const isMonthly = CF_PERIOD === 'mo'; const perLabel = isMonthly ? '/mo' : '/yr'; // Monthly: filter to selected month. Annual: filter to selected FY. const cfMonth = (document.getElementById('cf-month-sel') || {}).value || new Date().toISOString().slice(0,7); const cfFY = (document.getElementById('cf-fy-sel') || {}).value || currentFY(); const cfFYMonths = fyMonths(cfFY); const filterI = r => isMonthly ? r.month === cfMonth : cfFYMonths.includes(r.month); const filterE = r => isMonthly ? r.month === cfMonth : cfFYMonths.includes(r.month); // Loan payment = sum of "Mortgage" expense rows for the period (from statements). // Other expenses = all other expense rows for the period. const cfData = getFilteredProps('cf').map(p => { const incRows = IR.filter(r => r.prop === p.nick && filterI(r)); const expRows = ER.filter(r => r.prop === p.nick && filterE(r)); const stmtInc = incRows.reduce((a,r) => a+r.amt, 0); const loanPmt = expRows.filter(r => r.cat === 'Mortgage').reduce((a,r) => a+r.amt, 0); const otherExp = expRows.filter(r => r.cat !== 'Mortgage').reduce((a,r) => a+r.amt, 0); const stmtExp = loanPmt + otherExp; const hasStmt = incRows.length > 0 || expRows.length > 0; const netCf = stmtInc - stmtExp; const propCapex = CX.filter(cx=>cx.pid===p.id&&cxFilter(cx)).reduce((a,cx)=>a+cx.amt,0); return { p, stmtInc, stmtExp, loanPmt, otherExp, hasStmt, netCf, propCapex }; }); const portInc = cfData.reduce((a,d) => a + d.stmtInc, 0); const portExp = cfData.reduce((a,d) => a + d.stmtExp, 0); const portLoan = cfData.reduce((a,d) => a + d.loanPmt, 0); const portNet = cfData.reduce((a,d) => a + d.netCf, 0); const portCapex = cfData.reduce((a,d)=>a+d.propCapex,0); const hasAnyStmt = cfData.some(d => d.hasStmt); const periodLabel = isMonthly ? (() => { const [y,m] = cfMonth.split('-'); return new Date(y,m-1).toLocaleDateString('en-AU',{month:'long',year:'numeric'}); })() : cfFY; el.innerHTML = `
${isMonthly ? `Showing statements recorded for ${periodLabel}. Loan payments are taken from Mortgage expense entries in that month.` : `Showing totals for ${periodLabel} (1 Jul – 30 Jun). Loan payments are from Mortgage expense entries. Prior year data is preserved and accessible by changing the year above.`}
Total income
${hasAnyStmt ? f(portInc) : '—'}
${hasAnyStmt ? 'from statements' : 'record income in statements'}
Loan payments
${f(portLoan)}
Mortgage expense entries
Other expenses
${f(portExp - portLoan)}
all non-mortgage expenses
Capital expenses
${portCapex?f(portCapex):'—'}
this period
Net cashflow
${portNet>=0?'+':''}${f(portNet)}
${portNet < 0 ? 'Negative gearing' : 'Positive cashflow'}
${cfData.map(({p, stmtInc, stmtExp, loanPmt, otherExp, hasStmt, netCf, propCapex}) => { const isPos = netCf >= 0; const barMax = Math.max(stmtInc || 0, stmtExp, 1); const incPct = Math.min((stmtInc / barMax) * 100, 100); const expPct = Math.min((stmtExp / barMax) * 100, 100); const lp = LEASE[p.id]; return `
${esc(p.nick)}
${esc(p.addr)}${(!lp||!lp.tenant)?' · Vacant':''}
${netCfInclCapex>=0?'+':''}${f(netCfInclCapex)}${perLabel}
${hasStmt ? `
Income
${f(stmtInc)}
Expenses
${f(stmtExp)}
` : `
No statement entries for this period.
`}
Income
${hasStmt ? f(stmtInc) : '—'}
from statements
Mortgage
${f(loanPmt)}
loan repayments
Other costs
${f(otherExp)}
PM, rates, ins…
Net
${isPos?'+':''}${f(netCf)}
income − expenses
`; }).join('')}
Portfolio summary ${periodLabel}
${cfData.map(({p, stmtInc, stmtExp, loanPmt, otherExp, hasStmt, netCf}) => ``).join('')}
Property Income Mortgage Other costs Total expenses Net cashflow
${esc(p.nick)}
${hasStmt ? f(stmtInc) : ''} ${f(loanPmt)} ${f(otherExp)} ${f(stmtExp)} ${netCf>=0?'+':''}${f(netCf)}
Portfolio total ${hasAnyStmt ? f(portInc) : '—'} ${f(portLoan)} ${f(portExp - portLoan)} ${f(portExp)} ${portNet>=0?'+':''}${f(portNet)}
`; } // ============================================================= // TAX & REPORTS // ============================================================= function initFYPills() { const el = document.getElementById('fy-pills'); if (!el) return; const fys = allFYs().reverse(); el.innerHTML = fys.map((fy, i) => `` ).join(''); } function setFY(fy, el) { CUR_FY = fy; document.querySelectorAll('#fy-pills .pill').forEach(x=>x.classList.remove('active')); el.classList.add('active'); renderTax(); } // Returns per-property tax data for a given FY function propTaxData(p, fy) { const fyYear = parseInt(fy.split('-')[0].replace('FY','')) || 2025; const fyMths = fyMonths(fy); const incRows = IR.filter(r => r.prop === p.nick && fyMths.includes(r.month)); const expRows = ER.filter(r => r.prop === p.nick && fyMths.includes(r.month)); const hasStmt = incRows.length > 0 || expRows.length > 0; const income = incRows.reduce((a,r) => a+r.amt, 0); // All recorded expenses treated as fully deductible — no interest estimation const expenses = hasStmt ? expRows.reduce((a,r) => a+r.amt, 0) : propExpenses(p) * 12; // fallback: annualise monthly estimates let dep = 0; // Use DEP_DATA (manual entries from depreciation section) if available if (window.DEP_DATA) { const depKey = p.id + '_' + fy; const depEntry = DEP_DATA[depKey]; if (depEntry && (depEntry.div43 || depEntry.div40)) { dep = (Number(depEntry.div43)||0) + (Number(depEntry.div40)||0); } else { // Fall back to calculated method if (p.build && p.buildyr && fyYear - p.buildyr < 40) dep += p.build * 0.025; CX.filter(c=>c.pid===p.id&&c.treat==='div43').forEach(c=>{const yr=parseInt((c.date||'').match(/\d{4}/)?.[0])||fyYear;if(fyYear-yr<40)dep+=c.amt*0.025;}); CX.filter(c=>c.pid===p.id&&c.treat==='div40').forEach(c=>{const yr=parseInt((c.date||'').match(/\d{4}/)?.[0])||fyYear;const life=c.life||10;if(fyYear-yrc.pid===p.id&&c.treat==='div43').forEach(c=>{const yr=parseInt((c.date||'').match(/\d{4}/)?.[0])||fyYear;if(fyYear-yr<40)dep+=c.amt*0.025;}); CX.filter(c=>c.pid===p.id&&c.treat==='div40').forEach(c=>{const yr=parseInt((c.date||'').match(/\d{4}/)?.[0])||fyYear;const life=c.life||10;if(fyYear-yrSelect properties', '', ...PS.map(p => [ '' ].join('')) ].join(''); setTimeout(() => document.addEventListener('click', function _close(e) { const d = document.getElementById(screenId + '-prop-dropdown'); const b = document.getElementById(screenId + '-prop-filter-btn'); if (d && !d.contains(e.target) && b && !b.contains(e.target)) { d.style.display = 'none'; document.removeEventListener('click', _close); } }), 100); } else { dd.style.display = 'none'; } } function onPropFilterAll(cb, screenId) { const sel = getPropFilter(screenId); if (cb.checked) { sel.clear(); document.querySelectorAll('.pf-cb-' + screenId).forEach(x => x.checked = true); } updatePropFilterCount(screenId); const r1 = getScreenRenderer(screenId); if (r1) r1(); } function onPropFilterChange(screenId) { const sel = getPropFilter(screenId); sel.clear(); document.querySelectorAll('.pf-cb-' + screenId).forEach(cb => { if (cb.checked) sel.add(parseInt(cb.value)); }); if (sel.size === PS.length) sel.clear(); const allCb = document.getElementById(screenId + '-all-cb'); if (allCb) allCb.checked = sel.size === 0; updatePropFilterCount(screenId); const r1 = getScreenRenderer(screenId); if (r1) r1(); } function updatePropFilterCount(screenId) { const el = document.getElementById(screenId + '-prop-count'); const sel = getPropFilter(screenId); if (el) el.textContent = sel.size === 0 ? 'All' : sel.size; } // Keep old tax-specific functions as aliases for backward compatibility function toggleTaxPropFilter() { togglePropFilter('tax'); } function getSelectedProps() { return getFilteredProps('tax'); } function TAX_SELECTED_PROPS_compat() { return getPropFilter('tax'); } function renderTax() { const el = document.getElementById('tax-content'); if (!el) return; if (!PS.length) { el.innerHTML = emptyProps(); return; } const tax = PROF.tax || 0.37; const fy = CUR_FY; const fyYear = parseInt(fy.split('-')[0].replace('FY','')) || 2025; const selectedPS = getSelectedProps(); const propData = selectedPS.map(p => ({ p, ...propTaxData(p, fy) })); const grossInc = propData.reduce((a,d) => a+d.income, 0); const totalExp = propData.reduce((a,d) => a+d.expenses, 0); let totalDep = 0; propData.forEach(d => totalDep += d.dep); const deductible = totalExp + totalDep; // Use capped net for properties with negGearing=false const taxableInc = propData.reduce((a,d) => a+d.net, 0); const taxPayable = Math.max(0, taxableInc * tax); const taxSaved = Math.max(0, -taxableInc) * tax; const useStmt = propData.some(d => d.hasStmt); el.innerHTML = `
Estimates only — consult your accountant. All recorded expenses (including mortgage repayments) are shown as deductible. Your accountant will adjust for non-deductible principal components. Only ${fy} statement entries are included.
Gross rental income
${f(grossInc)}
${useStmt?'from '+fy+' statements':'no statements yet'}
Total deductions
${f(totalExp)}
excl. depreciation
Depreciation
${f(totalDep)}
Div 43 + Div 40
Net taxable income
${taxableInc>=0?'+':'-'}${fTax(Math.abs(taxableInc))}
${taxableInc<0?'Negative gearing (loss)':'Positive position'}
${taxableInc<0?'Tax saving @ '+Math.round(tax*100)+'%':'Tax payable @ '+Math.round(tax*100)+'%'}
${fTax(taxableInc<0?taxSaved:taxPayable)}
${taxableInc<0?'Benefit from losses':'Amount owing'}
Deduction breakdown — ${fy}
${[ ['Rental income',grossInc,'var(--green)'], ['Total expenses (all recorded)',totalExp,'var(--red)'], ['Depreciation (Div 40+43)',totalDep,'var(--blue)'], ['Net taxable',taxableInc,taxableInc<=0?'var(--green)':'var(--red)'], ].map(([l,v,col])=>``).join('')}
ItemAmount
${l}${v<0?'-':''}${f(Math.abs(v))}
Per-property breakdown Click PDF to download individual report
${propData.map(({p, income, expenses, dep, net, hasStmt}) => { return ``; }).join('')}
PropertyIncomeExpensesDepreciationNet taxableReport
${esc(p.nick)}${(p.ownerName||p.ownership)&&(p.ownerName||p.ownership||true)?" "+esc(p.ownerName||(p.ownership||"Personal name"))+"":""}
${f(income)} ${f(expenses)} ${f(dep)} ${net>=0?'+':'-'}${f(Math.abs(net))}
`; } // ============================================================= // PDF EXPORT — per property per FY (with appended documents) // ============================================================= function buildPropertyPDFHTML(p, fy) { const data = propTaxData(p, fy); const tax = PROF.tax || 0.37; const taxableInc = data.income - data.expenses - data.dep; const taxEffect = Math.abs(taxableInc) * tax; // Docs attached to this property (general docs section) // Only include: income/expense statement attachments, depreciation docs, capex docs const rowDocs = [...data.incRows, ...data.expRows] .filter(r => r.docId) .map(r => DS.find(d => d.id === r.docId)) .filter(Boolean) .filter(d => d.data || d.serverUrl || d.cloudUrl); const depDocs = DS.filter(d => d.pid === p.id && d.section === 'dep' && (d.data || d.serverUrl || d.cloudUrl)); const cxDocs = DS.filter(d => d.pid === p.id && d.section === 'cx' && (d.data || d.serverUrl || d.cloudUrl)); // All unique docs to append const allDocIds = new Set(); const allDocs = []; [...rowDocs, ...depDocs, ...cxDocs].forEach(d => { if (!allDocIds.has(d.id)) { allDocIds.add(d.id); allDocs.push(d); } }); const incomeRows = data.incRows.map(r => { const doc = r.docId ? DS.find(d=>d.id===r.docId) : null; return ` ${esc(r.month)}${esc(r.cat||'')}${esc(r.desc||'')}${doc?` 📎`+esc(doc.title||doc.name):''} $${Math.round(r.amt).toLocaleString('en-AU')} `; }).join(''); const expenseRows = data.expRows.map(r => { const doc = r.docId ? DS.find(d=>d.id===r.docId) : null; return ` ${esc(r.month)}${esc(r.cat||'')}${esc(r.desc||'')}${doc?` 📎`+esc(doc.title||doc.name):''} $${Math.round(r.amt).toLocaleString('en-AU')} `; }).join(''); const depRows = (() => { const fyYear = parseInt(fy.split('-')[0].replace('FY','')) || 2025; const rows = []; if (p.build && p.buildyr && fyYear - p.buildyr < 40) rows.push(`Building structure (Div 43)Div 43$${Math.round(p.build*0.025).toLocaleString('en-AU')}`); CX.filter(c=>c.pid===p.id&&c.treat==='div43').forEach(c=>{const yr=parseInt((c.date||'').match(/\d{4}/)?.[0])||fyYear;if(fyYear-yr<40)rows.push(`${esc(c.desc)}Div 43$${Math.round(c.amt*0.025).toLocaleString('en-AU')}`);}); CX.filter(c=>c.pid===p.id&&c.treat==='div40').forEach(c=>{const yr=parseInt((c.date||'').match(/\d{4}/)?.[0])||fyYear;const life=c.life||10;if(fyYear-yr${esc(c.desc)}Div 40 (${life}yr)$${Math.round(c.amt/life).toLocaleString('en-AU')}`);}); return rows.join(''); })(); const appendedPages = allDocs.map(d => { if (!d.data) { return `
📎 ${esc(d.title||d.name)} (${esc(d.type||'Document')})

Document could not be loaded for this report.

`; } else if (d.data.startsWith('data:image/')) { return `
📎 ${esc(d.title||d.name)} (${esc(d.type||'Document')})
`; } else if (d.data.startsWith('data:application/pdf')) { return `
📎 ${esc(d.title||d.name)} (${esc(d.type||'Document')} — PDF)

PDF invoice attached below. If it does not display, print this page and the PDF will be included.

PDF cannot be displayed inline in this browser. The file is embedded in this report.

`; } else { return `
📎 ${esc(d.title||d.name)} (${esc(d.type||'Document')})

This file type cannot be rendered inline.

`; } }).join(''); return ` ${esc(p.nick)} — ${esc(fy)} Tax Report

${esc(p.nick)}

${esc(p.addr||'')}${p.state?' · '+esc(p.state):''} · Generated ${new Date().toLocaleDateString('en-AU',{day:'numeric',month:'long',year:'numeric'})}
${PROF.name ? `
Investor: ${esc(PROF.name)}
` : ''}
${esc(fy)} Tax Report
For accountant use only. All expenses as recorded are shown — your accountant will determine which components are deductible (e.g. interest vs principal on mortgage repayments). Depreciation is an estimate only. This is not tax advice.

Property details

Type${esc(p.type||'—')}
Beds${p.beds||'—'}
State${esc(p.state||'—')}
Est. value${p.val?'$'+Math.round(p.val).toLocaleString('en-AU'):'—'}
Loan balance${p.loan?'$'+Math.round(p.loan).toLocaleString('en-AU'):'—'}
Build year${p.buildyr||'—'}

Financial summary — ${esc(fy)}

Gross rental income
$${Math.round(data.income).toLocaleString('en-AU')}
Total expenses (all recorded)
$${Math.round(data.expenses).toLocaleString('en-AU')}
Depreciation (Div 40+43)
$${Math.round(data.dep).toLocaleString('en-AU')}
Net position
${taxableInc<=0?'-':''}$${Math.round(Math.abs(taxableInc)).toLocaleString('en-AU')}
Gross rental income$${Math.round(data.income).toLocaleString('en-AU')}
Less: total expenses($${Math.round(data.expenses).toLocaleString('en-AU')})
Less: depreciation($${Math.round(data.dep).toLocaleString('en-AU')})
Net taxable position${taxableInc<=0?'(Loss) -':''}$${Math.round(Math.abs(taxableInc)).toLocaleString('en-AU')}
Est. tax effect @ ${Math.round((PROF.tax||0.37)*100)}% marginal rate${taxableInc<=0?'Saving ':'Payable '}$${Math.round(taxEffect).toLocaleString('en-AU')}

Income — ${esc(fy)}

${data.incRows.length ? `${incomeRows}
MonthCategoryDescriptionAmount
Total income$${Math.round(data.income).toLocaleString('en-AU')}
` : `

No income recorded for ${esc(fy)}.

`}

Expenses — ${esc(fy)}

All recorded expenses shown in full. Your accountant will separate deductible interest from non-deductible principal.

${data.expRows.length ? `${expenseRows}
MonthCategoryDescriptionAmount
Total expenses$${Math.round(data.expenses).toLocaleString('en-AU')}
` : `

No expenses recorded for ${esc(fy)}.

`}
${data.dep > 0 ? `

Depreciation schedule — ${esc(fy)}

${depRows}
ItemTypeAnnual deduction
Total depreciation$${Math.round(data.dep).toLocaleString('en-AU')}
` : ''} ${allDocs.length ? `
Attached documents (${allDocs.length})
    ${allDocs.map(d => `
  • ${esc(d.title||d.name)} — ${esc(d.type||'Document')} (${d.size?Math.round(d.size/1024)+'KB':'?KB'})
  • `).join('')}
${appendedPages}` : ''} `; } async function exportPropertyPDF(pid, fy) { const p = PS.find(x => x.id === pid); if (!p) return; toast('⏳ Fetching documents…', 6000); // Fetch all docs for this property const propDocIds = new Set(); DS.filter(d => d.pid === pid).forEach(d => propDocIds.add(d.id)); [...IR, ...ER].filter(r => r.docId && DS.find(d => d.id === r.docId && d.pid === pid)).forEach(r => propDocIds.add(r.docId)); for (const docId of propDocIds) { const d = DS.find(x => x.id === docId); if (d && !d.data && (d.serverUrl || d.cloudUrl)) { try { const fetched = await fetchDocFromServer(d.serverUrl || d.cloudUrl); if (fetched) d.data = fetched; } catch(e) {} } } const reportHtml = buildPropertyPDFHTML(p, fy); const blob = new Blob([reportHtml], { type: 'text/html' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = (p.nick.replace(/[^a-z0-9]/gi,'_') + '_' + fy + '_TaxReport.html'); document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 5000); toast('✓ Report downloaded — open the file and Print → Save as PDF', 5000); setTimeout(() => { DS.filter(d => propDocIds.has(d.id)).forEach(d => { d.data = null; }); }, 10000); } async function exportTaxPDFs() { const selectedPS = getSelectedProps(); if (!selectedPS.length) { toast('No properties to export'); return; } const fy = CUR_FY; if (selectedPS.length === 1) { // Single property — download directly await exportPropertyPDF(selectedPS[0].id, fy); return; } // Multiple properties — build combined PDF HTML toast('⏳ Fetching documents for PDF…', 8000); // Fetch all docs for all selected properties for (const p of selectedPS) { const propDocIds = new Set(); DS.filter(d => d.pid === p.id).forEach(d => propDocIds.add(d.id)); [...IR, ...ER].filter(r => r.docId && DS.find(d => d.id === r.docId && d.pid === p.id)).forEach(r => propDocIds.add(r.docId)); for (const docId of propDocIds) { const d = DS.find(x => x.id === docId); if (d && !d.data && (d.serverUrl || d.cloudUrl)) { try { const fetched = await fetchDocFromServer(d.serverUrl || d.cloudUrl); if (fetched) d.data = fetched; } catch(e) {} } } } // Combine all property reports into one HTML document const combinedSections = selectedPS.map(p => buildPropertyPDFHTML(p, fy) .replace('', '') .replace(/<\/html>$/, '') .replace(/[\s\S]*?<\/head>/, '') .replace('', '
') .replace('', '
') ).join(''); const firstReport = buildPropertyPDFHTML(selectedPS[0], fy); const styleMatch = firstReport.match(/