πŸ“ˆ My own RentReel portfolio snapshot (for demos/screenshots) β€” click to expand
Revenue: ${fmt(rev(txns(undefined,'all')))} Β· Expenses: ${fmt(exp(txns(undefined,'all')))}
Properties: ${(PROPS||[]).length} across ${(LLCS||[]).length} LLCs Β· Hours logged: ${((D.participation?.hours)||[]).length}
Metrics generated ${new Date(stats.generated_at).toLocaleString()} Β· Auto-refreshes on each page visit
`; // Store customer list for the detail modal window._founderCustomers=custs; } function showCustomerDetail(id){ const c=(window._founderCustomers||[]).find(x=>x.id===id); if(!c)return; ensureCustModal(); const days=Math.floor(c.days_on||0); const mrr=c.free_forever?0:(TIER_MRR[c.tier]||0); const html=`
${escHtml(c.email)}
${escHtml(c.id)}
Joined: ${new Date(c.created_at).toLocaleDateString()} Β· ${days} ${days===1?'day':'days'} on RentReel
Workspace: ${escHtml(c.workspace_name||'(not set)')}
Plan: ${(c.tier||'free').toUpperCase()}${c.free_forever?' · 🌟 FREE FOREVER VIP':(c.beta_lifetime?' · $19/mo Beta Lifetime Lock':'')}
Projected MRR: $${mrr}${c.free_forever?' (comped)':''}
Promo redeemed: ${c.promo_code?`${escHtml(c.promo_code)}`:'none'}
Referred by: ${c.referred_by?`${escHtml(c.referred_by)}`:'none'}
Own referral code: ${c.referral_code?`${escHtml(c.referral_code)}`:'none yet'}
Portfolio: ${c.property_count||0} properties Β· ${(c.txn_count||0).toLocaleString()} transactions
Last activity: ${c.last_updated?new Date(c.last_updated).toLocaleString():'never'}
🎁 Grant Promo Code
Updates this customer's plan + promo status immediately. They'll see it next time they refresh the app.
`; document.getElementById('cust-modal').innerHTML=html; document.getElementById('cust-modal').style.display='flex'; } async function grantPromo(userId){ const sel=document.getElementById('grant-code-'+userId); const code=sel?.value; if(!code)return; const info=KNOWN_PROMO_CODES[code]; if(!info){toast('Invalid code');return;} if(!confirm(`Grant "${code}" to this customer?\n\n${info.msg}`))return; try{ const {data,error}=await SB.rpc('founder_grant_promo',{ target_user_id:userId, code:code, tier:info.tier, beta_lifetime:info.betaLifetime, free_forever:!!info.freeForever, days_valid:info.days }); if(error){toast('Failed: '+error.message);return;} if(data?.error){toast('Failed: '+data.error);return;} toast(`βœ“ Granted ${code} to ${data.target_email}`); closeCustModal(); rFounder(); // refresh the console }catch(e){toast('Error: '+e.message);} } function ensureCustModal(){ if(document.getElementById('cust-modal'))return; const m=document.createElement('div');m.id='cust-modal'; m.style.cssText='display:none;position:fixed;inset:0;background:rgba(0,0,0,.85);backdrop-filter:blur(8px);z-index:1000;align-items:center;justify-content:center;padding:20px'; m.onclick=(e)=>{if(e.target.id==='cust-modal')closeCustModal();}; document.body.appendChild(m); } function closeCustModal(){const m=document.getElementById('cust-modal');if(m)m.style.display='none';} // ═══ CPA READ-ONLY SHARE ═══ // Encodes the current portfolio state into a compressed URL fragment so a CPA can open a // read-only view of the client's Schedule E without needing to sign up. Cursor-safe: the fragment // stays local (never sent to server, never logged in server access logs). function generateCpaShareUrl(year){ const Y=year||activeYear(); const relevantTxns=[...D.transactions,...(D.manual||[])] .filter(t=>new Date(t.date).getFullYear()===Y) .map(t=>({d:t.date,p:t.payee||t.desc,c:t.c,s:t.s,a:t.amount,t:t.t,pid:t.pid})); const payload={ v:1,year:Y,generatedAt:new Date().toISOString(), bizName:bizName()||'', props:PROPS.map(p=>({id:p.id,name:p.name,llc:p.llc,short:p.short||'',partner:p.partner||null,type:propType(p)})), llcs:LLCS.map(l=>({id:l.id,name:l.name,props:l.props,partner:!!l.partner})), txns:relevantTxns, miles:D.miles.filter(m=>(m.date||'').slice(0,4)===String(Y)).map(m=>({d:m.date,mi:m.miles,f:m.from,to:m.to,p:m.purpose})), mileRate:mileRate?.()||0.725 }; const json=JSON.stringify(payload); const encoded=btoa(unescape(encodeURIComponent(json))).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,''); return `${window.location.origin}${window.location.pathname}#cpa=${encoded}`; } // Write both text/plain (raw URL) and text/html (hyperlinked anchor) to clipboard. // When pasted into Gmail/Apple Mail/rich editors β†’ nice clickable link. // When pasted into plain text (Terminal, Notes plain) β†’ raw URL. async function _clipboardWriteRich(plainText, htmlText, successMsg){ try{ if(navigator.clipboard&&navigator.clipboard.write&&typeof ClipboardItem!=='undefined'){ const item=new ClipboardItem({ 'text/plain':new Blob([plainText],{type:'text/plain'}), 'text/html':new Blob([htmlText],{type:'text/html'}) }); await navigator.clipboard.write([item]); toast(successMsg); return; } if(navigator.clipboard&&navigator.clipboard.writeText){ await navigator.clipboard.writeText(plainText); toast(successMsg+' (plain text)'); return; } }catch(e){/* fall through */} prompt('Copy this:',plainText); } function copyCpaShareUrl(year){ const url=generateCpaShareUrl(year); const Y=year||activeYear(); const linkText=`πŸ“‹ Read-only view of my Schedule E ${Y} prep β†’`; const html=`

${linkText}

`; _clipboardWriteRich(url, html, 'Link copied βœ“ paste into email β€” auto-hyperlinks'); } function copyCpaEmailTemplate(year){ const url=generateCpaShareUrl(year); const Y=year||activeYear(); const biz=bizName()||'my rental portfolio'; const linkText=`πŸ“‹ Read-only view of my Schedule E ${Y} prep β†’`; const plainBody=`Hi,\n\nHere's a read-only link to review my ${Y} Schedule E prep. No login needed β€” just click to open in your browser:\n\n${linkText}\n${url}\n\nThe link contains a snapshot of my books as of today. Nothing is stored on any server β€” the data lives in the URL itself. Please review at your convenience and reply with any questions.\n\nAlso β€” I have a free lifetime Pro+ Tax account waiting for you if you want to explore RentReel yourself. Use promo code CPAREVIEW at app.rentreel.co.\n\nThanks!`; const htmlBody=`

Hi,

Here's a read-only link to review my ${escHtml(Y)} Schedule E prep for ${escHtml(biz)}. No login needed β€” just click to open in your browser:

${linkText}

The link contains a snapshot of my books as of today. Nothing is stored on any server β€” the data lives in the URL itself. Please review at your convenience and reply with any questions.

Also β€” I have a free lifetime Pro+ Tax account waiting for you if you want to explore RentReel yourself. Use promo code CPAREVIEW at app.rentreel.co.

Thanks!

`; _clipboardWriteRich(plainBody, htmlBody, 'Email template copied βœ“ paste into Gmail/Mail compose'); } function cpaShareCardHtml(){ return `
🀝 Share with your CPA
Generate a read-only link your CPA can open in their browser β€” no signup required. Perfect for tax-season review.
How it works: the link contains a snapshot of your ${activeYear()} Schedule E β€” properties, LLCs, transactions, mileage. Your CPA opens it in any browser and sees a clean read-only view. Nothing hits any server; the data lives entirely in the URL fragment (never logged, never tracked).

πŸ’‘ Pre-hyperlinked: both buttons copy in rich format β€” pastes into Gmail/Apple Mail as a clean clickable "Read-only view β†’" instead of the raw 30KB URL.

Also send them: the promo code CPAREVIEW β€” free lifetime Pro+ Tax account so they can explore RentReel themselves.
`; } // Handle ?cpa-demo=1 URL param on load β€” seed demo data into memory, then render the CPA read-only view. // Purpose: prospects (and Jordan) can click a single URL to see exactly what a CPA sees when a client shares their books. // Does NOT persist to storage β€” no cloud pull, no localStorage write. Pure in-memory preview. function checkCpaDemo(){ try{ const params=new URLSearchParams(window.location.search); if(params.get('cpa-demo')!=='1')return false; }catch(e){return false;} // CRITICAL: set global flag BEFORE any D mutation. This blocks save_local() + cloud.push() // so demo data never touches localStorage or Supabase. Real workspace stays intact. window.__CPA_DEMO_MODE__=true; // Seed demo data in-memory ONLY D.transactions=[];D.miles=[];D.manual=[];D.uploads=[];D.lastUp=null; D.props=DEFAULT_PROPS.map(p=>({...p})); D.llcs=DEFAULT_LLCS.map(l=>({...l,props:[...l.props]})); D.settings=D.settings||{};D.settings.workspaceName='Demo Portfolio (Sample Client)'; D.demoSeeded=false; loadPortfolio(); seedDemo(true); // Build the CPA read-only payload from the seeded workspace const Y=new Date().getFullYear(); const payload={ v:1,year:Y,generatedAt:new Date().toISOString(), bizName:'Demo Portfolio β€” Sample Client', props:(D.props||[]).map(p=>({id:p.id,name:p.name,llc:p.llc,short:p.short||'',partner:p.partner||null,type:p.type||'str'})), llcs:(D.llcs||[]).map(l=>({id:l.id,name:l.name,props:l.props,partner:!!l.partner})), txns:[...D.transactions,...(D.manual||[])].filter(t=>new Date(t.date).getFullYear()===Y).map(t=>({d:t.date,p:t.payee||t.desc,c:t.c,s:t.s,a:t.amount,t:t.t,pid:t.pid})), miles:(D.miles||[]).filter(m=>(m.date||'').slice(0,4)===String(Y)).map(m=>({d:m.date,mi:m.miles,f:m.from,to:m.to,p:m.purpose})), mileRate:0.725 }; renderCpaView(payload); // Add a banner explaining this is a demo view setTimeout(()=>{ if(document.getElementById('cpa-demo-banner'))return; const b=document.createElement('div');b.id='cpa-demo-banner'; b.style.cssText='position:fixed;top:0;left:0;right:0;background:linear-gradient(135deg,var(--yellow),var(--a4));color:#fff;padding:10px 16px;text-align:center;font-size:13px;font-weight:700;z-index:9999;box-shadow:0 4px 14px rgba(0,0,0,.15)'; b.innerHTML='πŸ‘€ CPA VIEW DEMO β€” This is exactly what a CPA sees when a RentReel client shares their books. Sample data only. ← Back to app'; document.body.appendChild(b); document.body.style.paddingTop='44px'; },100); return true; } // Handle #cpa= URL fragment on load β€” render read-only CPA view instead of the auth landing. function checkCpaHash(){ const hash=window.location.hash||''; const m=hash.match(/^#cpa=([A-Za-z0-9_-]+)$/); if(!m)return false; try{ const b64=m[1].replace(/-/g,'+').replace(/_/g,'/'); const json=decodeURIComponent(escape(atob(b64+'='.repeat((4-b64.length%4)%4)))); const p=JSON.parse(json); renderCpaView(p);return true; }catch(e){toast('Could not read shared link');return false;} } function cpaExportXlsx(payload){ // Build a lightweight multi-tab CSV bundle downloaded as a single .csv (CPAs can open in Excel). // Full .xlsx would require the SheetJS bundle in read-only mode β€” CSV works everywhere. const Y=payload.year; const rows=[['Schedule E Β· '+(payload.bizName||'Client')+' Β· Tax Year '+Y+' Β· Snapshot '+new Date(payload.generatedAt).toLocaleDateString()]]; rows.push([]); rows.push(['Line','Category','Property','LLC','Amount']); const sched={Cleaning:'Line 7',Repairs:'Line 14',Supplies:'Line 15',Utilities:'Line 17',Insurance:'Line 9',Professional:'Line 10',Management:'Line 11',Mortgage:'Line 12',Travel:'Line 6',Meals:'Line 19',Other:'Line 19',Taxes:'Line 16',Licenses:'Line 16'}; const propMap={};payload.props.forEach(p=>{propMap[p.id]={name:p.name,llc:p.llc};}); payload.txns.forEach(t=>{ const line=t.t==='income'?'Line 3':(sched[t.c]||'Line 19'); const prop=propMap[t.pid]||{name:'(unassigned)',llc:''}; rows.push([line,t.c,prop.name,prop.llc,t.a.toFixed(2)]); }); rows.push([]); rows.push(['PROPERTY TOTALS']); rows.push(['Property','LLC','Revenue','Expenses','Net']); payload.props.forEach(p=>{ const ts=payload.txns.filter(t=>t.pid===p.id); let r=0,e=0;ts.forEach(t=>{if(t.t==='income')r+=t.a;else if(t.t==='expense')e+=Math.abs(t.a);}); rows.push([p.name,p.llc,r.toFixed(2),e.toFixed(2),(r-e).toFixed(2)]); }); const csv=rows.map(r=>r.map(c=>{const s=String(c==null?'':c);return /[",\n]/.test(s)?'"'+s.replace(/"/g,'""')+'"':s;}).join(',')).join('\n'); const blob=new Blob([csv],{type:'text/csv'});const url=URL.createObjectURL(blob); const a=document.createElement('a');a.href=url;a.download=`schedule-e-${Y}-${(payload.bizName||'client').replace(/[^a-z0-9]+/gi,'-').toLowerCase()}.csv`; document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url); } function renderCpaView(payload){ // Hide the main app UI + auth landing const sb=document.getElementById('sidebar');if(sb)sb.style.display='none'; const mob=document.querySelector('.mob-nav');if(mob)mob.style.display='none'; const main=document.querySelector('.mn-w');if(main)main.style.display='block'; const auth=document.getElementById('auth-landing');if(auth)auth.style.display='none'; const banner=document.getElementById('demo-banner');if(banner)banner.style.display='none'; // Only pg-cpa visible document.querySelectorAll('[id^="pg-"]').forEach(p=>p.style.display='none'); const el=document.getElementById('pg-cpa');if(!el)return;el.style.display='block'; const gen=new Date(payload.generatedAt).toLocaleDateString('en-US',{dateStyle:'long'}); const totals={rev:0,exp:0}; payload.txns.forEach(t=>{if(t.t==='income')totals.rev+=t.a;else if(t.t==='expense')totals.exp+=Math.abs(t.a);}); const net=totals.rev-totals.exp; const perProp=payload.props.map(p=>{ const ts=payload.txns.filter(t=>t.pid===p.id); let r=0,e=0;ts.forEach(t=>{if(t.t==='income')r+=t.a;else if(t.t==='expense')e+=Math.abs(t.a);}); return{p,r,e,n:r-e,ts}; }); const sched=[ {line:'Line 3',label:'Rents received',cats:null,income:true}, {line:'Line 6',label:'Auto & travel',cats:['Travel']}, {line:'Line 7',label:'Cleaning & maintenance',cats:['Cleaning']}, {line:'Line 9',label:'Insurance',cats:['Insurance']}, {line:'Line 10',label:'Legal & professional',cats:['Professional']}, {line:'Line 11',label:'Management fees',cats:['Management']}, {line:'Line 12',label:'Mortgage interest',cats:['Mortgage']}, {line:'Line 14',label:'Repairs',cats:['Repairs']}, {line:'Line 15',label:'Supplies',cats:['Supplies']}, {line:'Line 16',label:'Taxes',cats:['Taxes','Licenses']}, {line:'Line 17',label:'Utilities',cats:['Utilities']}, {line:'Line 19',label:'Other (incl. meals)',cats:['Other','Meals']} ]; const totalMi=(payload.miles||[]).reduce((s,m)=>s+(+m.mi||0),0); const mileDed=totalMi*(payload.mileRate||0.725); el.innerHTML=`
${escHtml(payload.bizName||'Client')} Β· Schedule E Prep
Tax year ${payload.year} Β· Snapshot generated ${escHtml(gen)}
CPA Read-Only View
$${totals.rev.toLocaleString('en-US',{maximumFractionDigits:0})}
Total Revenue
$${totals.exp.toLocaleString('en-US',{maximumFractionDigits:0})}
Total Expenses
$${net.toLocaleString('en-US',{maximumFractionDigits:0})}
Net Income
${payload.props.length}
Properties
${perProp.map(pp=>`
${escHtml(pp.p.name)}
${escHtml(pp.p.llc)} Β· ${escHtml(pp.p.short||'')} Β· ${(pp.p.type||'str').toUpperCase()}${pp.p.partner?' Β· '+Math.round((1-(pp.p.partner.split||0.5))*100)+'% partner: '+escHtml(pp.p.partner.name||'β€”'):''}
${sched.map(s=>{if(s.income){return``;}const amt=Math.abs(pp.ts.filter(t=>t.t==='expense'&&s.cats?.includes(t.c)).reduce((sm,t)=>sm+t.a,0));if(amt===0)return'';return``;}).join('')}
LineCategoryAmount
${s.line}${s.label}$${pp.r.toFixed(2)}
${s.line}${s.label}$${amt.toFixed(2)}
Net Income / (Loss)$${pp.n.toFixed(2)}
`).join('')} ${totalMi>0?`
πŸš— Business Mileage
${(payload.miles||[]).length} trips Β· ${totalMi.toFixed(1)} miles Γ— $${(payload.mileRate||0.725).toFixed(3)}/mi = $${mileDed.toFixed(2)} deduction (Line 6)
`:''}
Reviewing this for a client?
RentReel is tax-optimized bookkeeping for STR + LTR operators. This snapshot is one property tax year β€” the app also handles REPS + Material Participation tracking, cost segregation estimation, and multi-tab Excel exports.
πŸ”‘ Free Pro+ Tax account Β· code CPAREVIEW
Β© ${new Date().getFullYear()} RentReel Β· Snapshot only β€” nothing is stored on any server Β· rentreel.co
`; } async function acctWipeData(){ if(!confirm('Wipe ALL your data?\n\nDeletes every transaction, upload, learned rule, hour log, asset/liability β€” keeps your account so you can start fresh. Cannot be undone.'))return; D.transactions=[];D.miles=[];D.manual=[];D.uploads=[];D.lastUp=null; D.learned={};D.learnedProp={};D.ambiguousPayees={};D.excluded=[]; D.participation={hours:[],otherParticipants:[],nonReHours:0,costBasis:{},marginalRate:0.32}; D.demoSeeded=true; save();rDash();rSettings();toast('All data wiped βœ“'); } async function acctDeleteAccount(){ if(!confirm('Delete your account?\n\nThis permanently deletes:\n β€’ All your transactions, properties, hours, mileage, uploaded documents\n β€’ Your authentication record (you cannot recover it)\n β€’ Any data stored in RentReel about you\n\nGDPR/CCPA compliant hard-delete. Cannot be undone.\n\nType YES on the next prompt to confirm.'))return; const confirmText=prompt('Type YES (all caps) to permanently delete your account:'); if(confirmText!=='YES'){toast('Deletion cancelled β€” nothing changed');return;} if(!cloud.user||!SB){toast('Not signed in');return;} toast('πŸ—‘οΈ Deleting account…'); // RPC handles workspace + personal_workspace + auth.users atomically (SECURITY DEFINER). const {data,error}=await SB.rpc('delete_my_account'); if(error){ console.error('delete_my_account error:',error); toast(`⚠️ Delete failed: ${error.message}. Email support@rentreel.co for manual removal.`); return; } // Successful hard-delete β€” clean up local state before signing out. D.transactions=[];D.miles=[];D.manual=[];D.uploads=[];D.props=undefined;D.llcs=undefined;D.lastUp=null; D.learned={};D.learnedProp={};D.ambiguousPayees={};D.excluded=[];D.participation=undefined;D.demoSeeded=true; save_local(); try{await cloud.signOut();}catch(e){} loadPortfolio(); localStorage.removeItem('strfinance_skip_auth'); rDash(); toast('βœ“ Account permanently deleted. All your data has been erased.'); showAuthLanding('signin'); } async function csSignOut(){ if(!confirm('Sign out? Your local data stays on this device.'))return; await cloud.signOut(); // Clear local data on sign-out so cousin/Dad can sign in cleanly on the same device D.transactions=[];D.miles=[];D.manual=[];D.uploads=[];D.props=undefined;D.llcs=undefined;D.lastUp=null;D.demoSeeded=true; save_local(); loadPortfolio(); localStorage.removeItem('strfinance_skip_auth'); rDash();rSettings(); toast('Signed out'); showAuthLanding('signin'); } // ═══ HELP / FAQ ═══ function rHelp(){ const el=document.getElementById('pg-help'); const sections=[ {title:'πŸš€ Getting Started',body:`
  1. Set up your portfolio in Settings β†’ Properties section. Add each STR with its bank account, LLC, and color.
  2. Upload your CSVs on the Upload page: reservations from any PMS (Hospitable, Guesty, Hostaway, OwnerRez, Lodgify) or Airbnb-direct, bank statements, credit cards, cleaning platform log, mileage tracker.
  3. No PMS? Skip that β€” just upload banks. RentReel treats Airbnb bank deposits as revenue automatically. Add reservations manually if you want STR ≀7-day qualification tracking.
  4. Review uncategorized on the Transactions page. Click any red badge (or bulk-select) to assign a category β€” the app learns.
  5. Export Schedule E when tax season hits. Excel file with one tab per property, ready for your CPA.
`}, {title:'πŸ“‚ Uploading CSVs β€” what gets auto-detected',body:`

Tip: Same CSV uploaded twice won't duplicate. The dedup key is date + amount + payee.

`}, {title:'🧠 Auto-Categorization & Learning',body:`

The app ships with 70+ built-in rules for common STR/LTR vendors β€” major mortgage servicers (Rocket, Mr. Cooper, Wells Fargo, Selene, NewRez), utilities (Duke Energy, PG&E, Xcel, Comcast, Spectrum), insurers (State Farm, Allstate, GEICO, Proper, Obie), cleaning brands (Merry Maids, Molly Maid, generic housekeeping), contractors (plumbing, HVAC, roofing, landscaping), platforms (Airbnb, VRBO, Booking.com, Hospitable), and PMS/software (Guesty, Hostaway, OwnerRez, Beyond Pricing, QuickBooks).

When you reassign a category for an unrecognized vendor, the app learns: future imports of that same payee auto-categorize, AND existing Uncategorized matches get back-applied.

Property learning works the same way β€” assign a payee to a property once, future ones route automatically. If you assign a payee to 2+ different properties, the app marks it ambiguous and stops auto-applying (forces manual assignment instead of silent mis-routing).

`}, {title:'πŸ”„ Smart Dedup β€” why bank Airbnb deposits are Transfers',body:`

If you upload a PMS reservations file AND bank statements, your books would double-count revenue without smart dedup. Here's why:

What the app does: your reservations import is the revenue source of truth. Bank Airbnb/VRBO/Booking/Hospitable Inc deposits get auto-tagged as "Platform Payout Β· Transfer" so they show on bank statements but don't double-count revenue. The host service fee from the reservations file gets auto-booked as a Management expense (Schedule E Line 11), keeping the math clean.

`}, {title:'πŸ“„ Schedule E Export',body:`

The Excel workbook your CPA receives has:

Hand the file to your accountant β€” no reformatting needed on their end.

`}, {title:'🎯 Tax Loophole Tracker (REPS + Material Participation)',body:`

If you're a real estate professional (or your spouse is W-2 and you offset their income via the STR loophole), this is the money page.

Real Estate Professional Status (REPS Β· IRC Β§469(c)(7)) requires:

If you qualify, rental losses become non-passive β€” they can offset W-2 income on a joint return.

STR loophole alternative (IRC Β§469 + Reg Β§1.469-1T): Material Participation per property with average customer use ≀ 7 days. Easiest test: 100+ hours AND more than anyone else (you vs. cleaner, contractor, co-host).

Cost Segregation can add significant Year 1 deductions on top β€” but any actual filing requires an engineered study by a qualified firm.

All numbers on this page are estimates. Log hours as you go (IRS audits look for contemporaneous records). Always consult a licensed CPA before filing.

IRS sources: Pub 925 Β· Pub 527

`}, {title:'πŸ’° 1099-NEC Vendor Tracking',body:`

Schedule E page auto-flags any non-corporate vendor you paid $600+ this year. Excludes utilities, mortgages, platforms (Airbnb/VRBO), and obvious corps.

Collect W-9s from flagged vendors before year-end. File 1099-NECs by Jan 31. The Excel export includes a "1099 Vendors" tab with blank W-9/TIN/Address columns for your CPA to fill.

`}, {title:'🚨 Common Issues & Fixes',body:``}, {title:'πŸ“§ Support & Feedback',body:`

Found a bug, have a feature idea, or need help with your books? Email support@rentreel.co or use the Send Feedback button in the sidebar.

For tax-specific questions, talk to your CPA. RentReel organizes your books and surfaces the loopholes β€” it doesn't give tax advice. If you need help finding an STR-savvy CPA, email hello@rentreel.co and we can share a shortlist.

`}, ]; el.innerHTML=`
Help & FAQ
How to use RentReel, troubleshoot common issues, and reach support
${sections.map((s,i)=>`
${s.title}
β–Ά
`).join('')}
`; } function rSettings(){ const el=document.getElementById('pg-settings'); const s=D.settings||{year:2026,mileRate:0.725,apiKey:''}; const txCount=D.transactions.length, mlCount=D.miles.length, mnCount=D.manual.length, upCount=D.uploads.length; const sizeKb=Math.round(JSON.stringify(D).length/1024); el.innerHTML=`
Settings
Preferences, integrations, and data backup
${cloudSyncCardHtml()} ${accountCardHtml()}
🎨 Theme
Pick the look that's easiest on your eyes.
${THEMES.map(t=>`
${t.sw.map(c=>`
`).join('')}
${t.name}
${(D.settings.theme||'light')===t.id?`
βœ“ ACTIVE
`:''}
`).join('')}
βš™οΈ Preferences
Shown in the sidebar, dashboard subtitle, Schedule E exports, and CPA workbook.
Used for YTD filters and Schedule E
2026 IRS standard: $0.725/mi
πŸ”‘ API Key
Stored locally in your browser. Reserved for future AI-assisted categorization.
πŸ”’ Your data & security
Row-Level Security (RLS) at the database. Every user gets an isolated row keyed to their auth ID. Postgres refuses to return one user's data to another β€” enforced at the DB level, not just in the app code.
HTTPS everywhere. All traffic encrypted in transit via Cloudflare TLS. Passwords are bcrypt-hashed by Supabase β€” we never see or store your plaintext password.
Where your data lives. Supabase Postgres, hosted on AWS US-East. SOC 2 Type 2 compliant. We don't sell, share, or use your data for advertising or AI training β€” ever.
CSV parsing runs in your browser. Only the parsed transaction rows leave your device. The original CSV file never touches our servers.
You own your data. Download a full JSON backup anytime via the button below. Wipe or delete your account permanently via the Account section β€” no support ticket required.
Full details in Privacy Policy Β· Report a security issue to support@rentreel.co
πŸ’Ύ Data Backup
${txCount} txns Β· ${mlCount} drives Β· ${mnCount} manual Β· ${upCount} uploads Β· ${sizeKb} KB
Export saves your entire local state as a JSON file. Import merges or replaces it. Use this to move data between devices or back up before tax season.
🏠 Properties
${PROPS.length} ${PROPS.length===1?'property':'properties'} Β· Edit, add, or remove without code changes
${PROPS.map(p=>{const txCount=D.transactions.filter(t=>t.pid===p.id).length;return`
${p.name}${p.partner?` ${Math.round((p.partner.split||0.5)*100)}/${100-Math.round((p.partner.split||0.5)*100)} ${p.partner.name||'Partner'}`:''}
${p.llc} Β· Acct ${p.acct} Β· ${p.short} Β· ${txCount} txns
`;}).join('')}
🏒 LLCs
${LLCS.length} ${LLCS.length===1?'LLC':'LLCs'}${bizName()?` under ${escHtml(bizName())}`:''} Β· Each holds one or more properties
${LLCS.map(l=>{const ps=PROPS.filter(p=>p.llc===l.name);return`
${l.name}${l.partner?` PARTNERSHIP`:''}
${ps.length} ${ps.length===1?'property':'properties'}${ps.length?': '+ps.map(p=>p.short||p.name).join(', '):''}
`;}).join('')}
🧠 Learned Categorization Rules
${Object.keys(D.learned||{}).length?``:''}
When you recategorize a transaction, the app remembers and applies it to future imports.
${Object.keys(D.learned||{}).length?`
${Object.entries(D.learned).sort((a,b)=>(b[1].ts||0)-(a[1].ts||0)).map(([key,r])=>`
${key}
${r.c}
`).join('')}
`:`
No category rules yet. Go to Transactions, click any category badge to reassign β€” it'll show up here.
`}
πŸ“ Learned Property Rules
${Object.keys(D.learnedProp||{}).length?``:''}
For commingled accounts: assign a payee to a property once (click the property cell in Transactions) and it auto-routes future imports.
${Object.keys(D.learnedProp||{}).length?`
${Object.entries(D.learnedProp).map(([key,pid])=>{const p=PROPS.find(x=>x.id===pid);return`
${key}
${p?p.short||p.name:'Unknown'}
`;}).join('')}
`:`
No property rules yet. Only needed if your bank account isn't dedicated to one property.
`}
πŸ“– Help
${Object.keys(D.helpDismissed||{}).length?``:''}
${Object.keys(D.helpDismissed||{}).length?`${Object.keys(D.helpDismissed).length} help banners hidden β€” click reset to show again.`:`Help banners appear on Transactions, Upload, and Schedule E pages explaining the app's smart features.`}
πŸ›  About RentReel
Business Name: ${bizName()||'(not set β€” add it in Preferences above)'}
Works with: any PMS (Hospitable, Guesty, Hostaway, OwnerRez, Lodgify, Airbnb-direct) Β· any bank Β· any cleaning platform Β· any mileage tracker
Exports: Schedule E .xlsx, per-property tabs, 1099 vendor summary β€” ready for any CPA
RentReel BETA Β· Single-file web app Β· Cloud-synced via Supabase
`; } function saveSettings(){ D.settings.year=parseInt(document.getElementById('st-year').value)||2026; D.settings.mileRate=parseFloat(document.getElementById('st-mile').value)||0.725; D.settings.workspaceName=document.getElementById('st-bizname').value.trim(); // Re-run categorization on Uncategorized transactions so newly added rules // (Related Entities, updated bizName, etc.) apply retroactively without a re-upload. let recatCount=0; (D.transactions||[]).forEach(t=>{ if(t.c!=='Uncategorized')return; const c=cat(t.payee,t.desc,t.amount); if(c.c!=='Uncategorized'){t.c=c.c;t.s=c.s;t.t=c.t;t.se=c.se;t.ok=true;recatCount++;} }); D.miles.forEach(m=>{m.ded=parseFloat((m.miles*D.settings.mileRate).toFixed(2));}); save();applyBizName(); toast(recatCount>0?`Preferences saved βœ“ Β· ${recatCount} transaction${recatCount===1?'':'s'} auto-categorized`:'Preferences saved βœ“'); rSettings();rDash(); } function saveApiKey(){ D.settings.apiKey=document.getElementById('st-key').value.trim(); save();toast('API key saved βœ“'); } function exportBackup(){ if(!guardDemoAction('export your data'))return; const json=JSON.stringify(D,null,2); dl(json,`rentreel-backup-${new Date().toISOString().slice(0,10)}.json`,'application/json'); toast('Backup exported βœ“'); } function importBackup(file){ if(!file)return; const r=new FileReader(); r.onload=e=>{ try{ const obj=JSON.parse(e.target.result); if(!obj||typeof obj!=='object'||!Array.isArray(obj.transactions)){toast('Invalid backup file');return;} if(!confirm(`Replace local data with ${obj.transactions.length} txns from backup?`))return; D=Object.assign({transactions:[],miles:[],manual:[],uploads:[],lastUp:null,settings:{year:2026,mileRate:0.725,apiKey:''}},obj); if(!D.settings)D.settings={year:2026,mileRate:0.725,apiKey:''}; save();rSettings();rDash();toast('Backup imported βœ“'); }catch(err){toast('Failed: '+err.message);} }; r.readAsText(file); } // ═══ PORTFOLIO MANAGER ═══ const PROP_COLORS=['#00ffb3','#4d94ff','#ff8c42','#b84dff','#ffb830','#4dffea','#ff4d7a','#00d4ff','#ff6b9d','#9d6bff']; function ensurePropModal(){ if(document.getElementById('prop-modal'))return; const m=document.createElement('div'); m.id='prop-modal'; m.style.cssText='display:none;position:fixed;inset:0;background:rgba(0,0,0,.8);backdrop-filter:blur(8px);z-index:1000;align-items:center;justify-content:center;padding:20px'; m.innerHTML='
' +'
Add Property
' +'
Properties are the unit of P&L. Each one rolls up to an LLC.
' +'
' +'
' +'
' +'
' +'
Determines which tax loopholes apply. STR gets Β§469 material participation; LTR falls under passive rules but REPS + Cost Seg still apply to both.
' +'
' +'
' +'' // ─── Portfolio & Finance section (Properties Hub Phase 1) ─── +'
' +'
' +'
πŸ’° PORTFOLIO & FINANCE (optional β€” enables net worth + mortgage split)
' +'
β–Έ
' +'
' +'' +'
' +'
'; document.body.appendChild(m); } function ensureLlcModal(){ if(document.getElementById('llc-modal'))return; const m=document.createElement('div'); m.id='llc-modal'; m.style.cssText='display:none;position:fixed;inset:0;background:rgba(0,0,0,.8);backdrop-filter:blur(8px);z-index:1000;align-items:center;justify-content:center;padding:20px'; m.innerHTML='
' +'
Add LLC
' +'
Each LLC files its own Schedule E and holds one or more properties.
' +'
' +'
' +'
' +'' +'
' +'
'; document.body.appendChild(m); } function pickColor(inputId,wrapId,selected){ const wrap=document.getElementById(wrapId);wrap.innerHTML=''; PROP_COLORS.forEach(c=>{ const d=document.createElement('div'); d.style.cssText=`width:28px;height:28px;border-radius:50%;background:${c};cursor:pointer;border:2px solid ${c===selected?'#fff':'transparent'};transition:transform .1s`; d.onclick=()=>{document.getElementById(inputId).value=c;pickColor(inputId,wrapId,c);}; wrap.appendChild(d); }); document.getElementById(inputId).value=selected; } function openPropModal(id){ ensurePropModal(); const llcSel=document.getElementById('pm-llc'); llcSel.innerHTML=LLCS.map(l=>``).join(''); if(id){ const p=PROPS.find(x=>x.id===id);if(!p)return; document.getElementById('pm-title').textContent='Edit Property'; document.getElementById('pm-id').value=p.id; document.getElementById('pm-name').value=p.name; document.getElementById('pm-short').value=p.short||''; document.getElementById('pm-acct').value=p.acct||''; document.getElementById('pm-llc').value=p.llc; document.getElementById('pm-type').value=propType(p); pickColor('pm-color','pm-colors',p.color); document.getElementById('pm-partner-on').checked=!!p.partner; document.getElementById('pm-partner-box').style.display=p.partner?'block':'none'; document.getElementById('pm-partner-name').value=p.partner?.name||''; document.getElementById('pm-partner-split').value=p.partner?Math.round(p.partner.split*100):50; // Address (for Zillow + mileage/hours auto-attribution) document.getElementById('pm-street').value=p.address?.street||''; document.getElementById('pm-city').value=p.address?.city||''; document.getElementById('pm-state').value=p.address?.state||''; document.getElementById('pm-zip').value=p.address?.zip||''; // Portfolio & Finance fields document.getElementById('pm-value').value=p.value||''; document.getElementById('pm-purchase-price').value=p.purchase?.price||''; document.getElementById('pm-purchase-date').value=p.purchase?.date||''; document.getElementById('pm-mortgage-servicer').value=p.mortgage?.servicer||''; document.getElementById('pm-mortgage-rate').value=p.mortgage?.rate||''; document.getElementById('pm-mortgage-balance').value=p.mortgage?.balance||''; document.getElementById('pm-mortgage-pi').value=p.mortgage?.monthlyPI||''; document.getElementById('pm-mortgage-escrow').value=p.mortgage?.escrow||''; document.getElementById('pm-mortgage-start').value=p.mortgage?.startDate||''; document.getElementById('pm-ins-carrier').value=p.insurance?.carrier||''; document.getElementById('pm-ins-premium').value=p.insurance?.annualPremium||''; document.getElementById('pm-ins-policy').value=p.insurance?.policyNumber||''; document.getElementById('pm-ins-renewal').value=p.insurance?.renewalDate||''; document.getElementById('pm-tax-annual').value=p.taxes?.annualAmount||''; document.getElementById('pm-notes').value=p.notes||''; // Auto-open Finance section when editing an existing property so address fields are visible document.getElementById('pm-finance-body').style.display='block'; document.getElementById('pm-finance-caret').textContent='β–Ύ'; }else{ document.getElementById('pm-title').textContent='Add Property'; document.getElementById('pm-id').value=''; document.getElementById('pm-type').value='str'; ['pm-name','pm-short','pm-acct','pm-partner-name'].forEach(k=>document.getElementById(k).value=''); document.getElementById('pm-partner-split').value=50; document.getElementById('pm-partner-on').checked=false; document.getElementById('pm-partner-box').style.display='none'; pickColor('pm-color','pm-colors',PROP_COLORS[PROPS.length%PROP_COLORS.length]); // Reset Finance fields for new property ['pm-street','pm-city','pm-state','pm-zip','pm-value','pm-purchase-price','pm-purchase-date','pm-mortgage-servicer','pm-mortgage-rate','pm-mortgage-balance','pm-mortgage-pi','pm-mortgage-escrow','pm-mortgage-start','pm-ins-carrier','pm-ins-premium','pm-ins-policy','pm-ins-renewal','pm-tax-annual','pm-notes'].forEach(k=>{const el=document.getElementById(k);if(el)el.value='';}); document.getElementById('pm-finance-body').style.display='none'; document.getElementById('pm-finance-caret').textContent='β–Έ'; } document.getElementById('prop-modal').style.display='flex'; } function closePropModal(){const m=document.getElementById('prop-modal');if(m)m.style.display='none';} function savePropModal(){ const id=document.getElementById('pm-id').value; const name=document.getElementById('pm-name').value.trim(); if(!name){toast('Property name required');return;} const short=document.getElementById('pm-short').value.trim()||name.split(' ').map(w=>w[0]).join('').slice(0,4).toUpperCase(); const acct=document.getElementById('pm-acct').value.trim(); const llc=document.getElementById('pm-llc').value; const color=document.getElementById('pm-color').value||PROP_COLORS[0]; const partnerOn=document.getElementById('pm-partner-on').checked; const partner=partnerOn?{name:document.getElementById('pm-partner-name').value.trim()||'Partner',split:(parseFloat(document.getElementById('pm-partner-split').value)||50)/100}:null; const type=document.getElementById('pm-type').value||'str'; // Portfolio & Finance const num=(k)=>{const v=parseFloat(document.getElementById(k).value);return isNaN(v)?null:v;}; const str=(k)=>{const v=document.getElementById(k).value.trim();return v||null;}; const value=num('pm-value'); const purchase=(num('pm-purchase-price')||str('pm-purchase-date'))?{price:num('pm-purchase-price'),date:str('pm-purchase-date')}:null; const mortgage=(num('pm-mortgage-balance')||num('pm-mortgage-rate')||str('pm-mortgage-servicer'))?{ servicer:str('pm-mortgage-servicer'),rate:num('pm-mortgage-rate'),balance:num('pm-mortgage-balance'), monthlyPI:num('pm-mortgage-pi'),escrow:num('pm-mortgage-escrow'),startDate:str('pm-mortgage-start') }:null; const insurance=(str('pm-ins-carrier')||num('pm-ins-premium'))?{ carrier:str('pm-ins-carrier'),annualPremium:num('pm-ins-premium'), policyNumber:str('pm-ins-policy'),renewalDate:str('pm-ins-renewal') }:null; const taxes=num('pm-tax-annual')?{annualAmount:num('pm-tax-annual')}:null; const notes=str('pm-notes'); const address=(str('pm-street')||str('pm-city')||str('pm-state')||str('pm-zip'))?{ street:str('pm-street'),city:str('pm-city'), state:(str('pm-state')||'').toUpperCase()||null,zip:str('pm-zip') }:null; const financeFields={address,value,purchase,mortgage,insurance,taxes,notes}; if(id){ const p=PROPS.find(x=>x.id===id);if(!p)return; const oldLlc=p.llc; Object.assign(p,{name,short,acct,llc,color,partner,type,...financeFields}); // Update transactions if LLC changed if(oldLlc!==llc){D.transactions.forEach(t=>{if(t.pid===id)t.llc=llc;});} // Update LLC props arrays LLCS.forEach(l=>{l.props=l.props.filter(x=>x!==id);}); const newLlc=LLCS.find(l=>l.name===llc);if(newLlc&&!newLlc.props.includes(id))newLlc.props.push(id); save();closePropModal();rSettings();rDash();toast('Property updated βœ“'); }else{ const newId='p_'+uid().slice(1); PROPS.push({id:newId,name,short,acct,llc,color,partner,type,...financeFields}); const newLlc=LLCS.find(l=>l.name===llc);if(newLlc)newLlc.props.push(newId); D.props=PROPS;D.llcs=LLCS; save();closePropModal();rSettings();rDash();toast(`${name} added βœ“`); } } function deleteProperty(id){ const p=PROPS.find(x=>x.id===id);if(!p)return; const txCount=D.transactions.filter(t=>t.pid===id).length; const mCount=D.manual.filter(m=>m.pid===id).length; let msg=`Delete "${p.name}"?`; if(txCount+mCount>0){msg+=`\n\n${txCount+mCount} transactions reference this property. They will be re-assigned to "Unassigned" (you can fix them later or reassign on import).`;} if(!confirm(msg))return; D.transactions.forEach(t=>{if(t.pid===id){t.pid=null;t.llc=null;}}); D.manual.forEach(m=>{if(m.pid===id){m.pid=null;m.llc=null;}}); PROPS=PROPS.filter(x=>x.id!==id); LLCS.forEach(l=>{l.props=l.props.filter(x=>x!==id);}); D.props=PROPS;D.llcs=LLCS; save();rSettings();rDash();toast(`${p.name} removed βœ“`); } function openLlcModal(id){ ensureLlcModal(); if(id){ const l=LLCS.find(x=>x.id===id);if(!l)return; document.getElementById('lm-title').textContent='Edit LLC'; document.getElementById('lm-id').value=l.id; document.getElementById('lm-name').value=l.name; pickColor('lm-color','lm-colors',l.color); document.getElementById('lm-partner').checked=!!l.partner; }else{ document.getElementById('lm-title').textContent='Add LLC'; document.getElementById('lm-id').value=''; document.getElementById('lm-name').value=''; document.getElementById('lm-partner').checked=false; pickColor('lm-color','lm-colors',PROP_COLORS[LLCS.length%PROP_COLORS.length]); } document.getElementById('llc-modal').style.display='flex'; } function closeLlcModal(){const m=document.getElementById('llc-modal');if(m)m.style.display='none';} function saveLlcModal(){ const id=document.getElementById('lm-id').value; const name=document.getElementById('lm-name').value.trim(); if(!name){toast('LLC name required');return;} const color=document.getElementById('lm-color').value||PROP_COLORS[0]; const partner=document.getElementById('lm-partner').checked; if(id){ const l=LLCS.find(x=>x.id===id);if(!l)return; const oldName=l.name; Object.assign(l,{name,color,partner}); // Cascade rename to PROPS + transactions PROPS.forEach(p=>{if(p.llc===oldName)p.llc=name;}); D.transactions.forEach(t=>{if(t.llc===oldName)t.llc=name;}); D.manual.forEach(m=>{if(m.llc===oldName)m.llc=name;}); save();closeLlcModal();rSettings();rDash();toast('LLC updated βœ“'); }else{ LLCS.push({id:'llc_'+uid().slice(1),name,color,partner,props:[]}); D.llcs=LLCS; save();closeLlcModal();rSettings();toast(`${name} added βœ“`); } } function deleteLlc(id){ const l=LLCS.find(x=>x.id===id);if(!l)return; const props=PROPS.filter(p=>p.llc===l.name); if(props.length){toast(`Reassign or delete properties first: ${props.map(p=>p.name).join(', ')}`);return;} if(!confirm(`Delete LLC "${l.name}"?`))return; LLCS=LLCS.filter(x=>x.id!==id);D.llcs=LLCS; save();rSettings();toast(`${l.name} removed βœ“`); } function reseedDemo(){ if(!confirm('Clear current data and reseed demo + sample portfolio? Cannot be undone.'))return; D.transactions=[];D.miles=[];D.manual=[];D.uploads=[];D.lastUp=null; // Reseed the sample 5-property / 3-LLC portfolio along with the transactions D.props=DEFAULT_PROPS.map(p=>({...p})); D.llcs=DEFAULT_LLCS.map(l=>({...l,props:[...l.props]})); loadPortfolio(); D.demoSeeded=false; // allow seed to run seedDemo(true);rSettings();rDash();toast('Demo data + sample portfolio loaded βœ“'); } // ═══ EXPORTS ═══ function dl(content,name,type='text/csv'){const b=new Blob([content],{type});const u=URL.createObjectURL(b);const a=document.createElement('a');a.href=u;a.download=name;a.click();URL.revokeObjectURL(u);} function exportReport(){ const s=propSums(curF.period);const at=txns(undefined,'all');const tR=rev(at),tE=exp(at),tN=tR-tE; let csv=`RENTREEL REPORT β€” ${(bizName()||'Rental Portfolio').toUpperCase()}\n`; csv+=`Period: ${periodLabel()} Β· Generated: ${new Date().toLocaleDateString()}\n\n`; csv+='PROPERTY,LLC,REVENUE,CLEANING,MORTGAGE,UTILITIES,OTHER,NET,MARGIN\n'; s.forEach(x=>csv+=`"${safeCell(x.p.name)}","${safeCell(x.p.llc)}",${x.r.toFixed(2)},${x.clean.toFixed(2)},${x.mort.toFixed(2)},${x.util.toFixed(2)},${x.other.toFixed(2)},${x.n.toFixed(2)},${x.margin}%\n`); csv+=`\nTOTAL,,,,,,,${tN.toFixed(2)},${tR>0?Math.round(tN/tR*100):0}%\n`; // Partner-split lines β€” data-driven for every property that has a co-owner (not one hardcoded property). PROPS.filter(p=>p.partner).forEach(p=>{ const pct=Math.round((p.partner.split||0.5)*100); csv+=`\n${safeCell(p.name)} β€” Your share (${pct}%): $${(getNet(txns(undefined,p.id))*(p.partner.split||0.5)).toFixed(2)} Β· ${safeCell(p.partner.name||'Partner')}'s share (${100-pct}%): $${(getNet(txns(undefined,p.id))*(1-(p.partner.split||0.5))).toFixed(2)}\n`; }); dl(csv,`rentreel-report-${curF.period}.csv`);toast('Report exported βœ“'); } function getNet(ts){return rev(ts)-exp(ts);} function exportPnL(){ const at=txns(undefined,'all');let csv='DATE,PROPERTY,LLC,PAYEE,CATEGORY,SUB,TYPE,AMOUNT,SOURCE\n'; at.sort((a,b)=>new Date(a.date)-new Date(b.date)).forEach(t=>{const p=PROPS.find(x=>x.id===t.pid);csv+=`"${safeCell(t.date)}","${safeCell(p?.name||'')}","${safeCell(p?.llc||'')}","${safeCell(t.payee||'')}","${safeCell(t.c)}","${safeCell(t.s||'')}","${t.t}",${t.amount.toFixed(2)},"${safeCell(t.src||'')}"\n`;}); dl(csv,`rentreel-pnl-${curF.period}.csv`);toast('P&L exported βœ“'); } function exportAll(){ if(!guardDemoAction('export your books'))return; const at=[...D.transactions,...D.manual].filter(t=>t.t!=='transfer');let csv='DATE,PAYEE,PROPERTY,ACCOUNT,CATEGORY,SUB,TYPE,AMOUNT,SOURCE\n'; at.sort((a,b)=>new Date(a.date)-new Date(b.date)).forEach(t=>{const p=PROPS.find(x=>x.id===t.pid);csv+=`"${safeCell(t.date)}","${safeCell(t.payee||'')}","${safeCell(p?.name||'')}","${safeCell(t.acct||'')}","${safeCell(t.c)}","${safeCell(t.s||'')}","${t.t}",${t.amount.toFixed(2)},"${safeCell(t.src||'')}"\n`;}); dl(csv,'all-transactions.csv');toast('CSV exported βœ“'); } // Schedule E line mapping const SCHE_LINES=[ {line:'Line 3',label:'Rents received',cats:null,income:true}, {line:'Line 5',label:'Advertising',cats:[]}, {line:'Line 6',label:'Auto & travel',cats:['Travel']}, {line:'Line 7',label:'Cleaning & maintenance',cats:['Cleaning']}, {line:'Line 9',label:'Insurance',cats:['Insurance']}, {line:'Line 10',label:'Legal & professional',cats:['Professional']}, {line:'Line 11',label:'Management fees',cats:['Management']}, {line:'Line 12',label:'Mortgage interest',cats:['Mortgage']}, {line:'Line 14',label:'Repairs',cats:['Repairs']}, {line:'Line 15',label:'Supplies',cats:['Supplies']}, {line:'Line 16',label:'Taxes',cats:['Taxes','Licenses']}, {line:'Line 17',label:'Utilities',cats:['Utilities']}, {line:'Line 19',label:'Other (incl. meals β€” CPA applies 50% rule)',cats:['Uncategorized','Other','Meals']}, ]; function exportXlsx(){ if(!guardDemoAction('export your Schedule E workbook'))return; if(typeof XLSX==='undefined'){toast('Excel library still loading…');return;} if(!D.settings)D.settings={};D.settings.hasExported=true;save(); const year=(D.settings&&D.settings.year)||2026; const mileRate=(D.settings&&D.settings.mileRate)||0.725; const wb=XLSX.utils.book_new(); const totMi=D.miles.reduce((s,m)=>s+m.miles,0); const mileDed=totMi*mileRate; const sums=propSums('ytd'); const tR=sums.reduce((s,x)=>s+x.r,0),tE=sums.reduce((s,x)=>s+x.e,0),tN=tR-tE; // ─── SUMMARY TAB ─── const sum=[ [`RENTREEL β€” ${(bizName()||'Rental Portfolio').toUpperCase()}`], [`Schedule E Summary Β· Tax Year ${year}`], [`Generated: ${new Date().toLocaleDateString()} β€” ready for your CPA`], [], ['PORTFOLIO TOTALS'], ['Total Revenue',tR], ['Total Expenses',tE], ['Net Income',tN], ['Net Margin',tR>0?(tN/tR):0], ['Vehicle Mileage Deduction',mileDed,'',`${Math.round(totMi)} mi Γ— $${mileRate}/mi`], [], ['PER-PROPERTY ROLLUP'], ['Property','LLC','Account','Revenue','Cleaning','Mortgage','Utilities','Other Exp','Total Exp','Net Income','Margin'], ]; sums.forEach(x=>{sum.push([x.p.name,x.p.llc,x.p.acct,x.r,x.clean,x.mort,x.util,x.other,x.e,x.n,x.r>0?(x.n/x.r):0]);}); sum.push(['TOTAL','','',tR,sums.reduce((s,x)=>s+x.clean,0),sums.reduce((s,x)=>s+x.mort,0),sums.reduce((s,x)=>s+x.util,0),sums.reduce((s,x)=>s+x.other,0),tE,tN,tR>0?(tN/tR):0]); sum.push([]); sum.push(['LLC BREAKDOWN']); sum.push(['LLC','Properties','Revenue','Expenses','Net','Notes']); LLCS.forEach(l=>{ const ps=PROPS.filter(p=>l.props.includes(p.id)); const t=llcTxns('ytd',l.name); const r=rev(t),e=exp(t); const partnerNote=l.partner?'Partnership LLC β€” split reported per property':(ps.length===0?'Holding company β€” shared overhead only':''); const propList=ps.length?ps.map(p=>p.name).join(', '):'(no properties β€” holding/overhead)'; sum.push([l.name,propList,r,e,r-e,partnerNote]); }); // Partner split callout β€” dynamic for any partnership property const partnerProps=PROPS.filter(p=>p.partner); if(partnerProps.length){ sum.push([]); sum.push(['PARTNER SPLIT SUMMARY']); partnerProps.forEach(prop=>{ const ts=txns('ytd',prop.id);const n=rev(ts)-exp(ts); const yourPct=Math.round((prop.partner.split||0.5)*100); const partnerPct=100-yourPct; const partnerName=prop.partner.name||'Partner'; sum.push([`${prop.name} Β· ${prop.llc} Β· ${yourPct}/${partnerPct} with ${partnerName}`]); sum.push(['Net Income',n]); sum.push([`Your Share (${yourPct}%)`,n*(prop.partner.split||0.5)]); sum.push([`${partnerName}'s Share (${partnerPct}%)`,n-(n*(prop.partner.split||0.5))]); }); } const ws1=XLSX.utils.aoa_to_sheet(sum); ws1['!cols']=[{wch:36},{wch:30},{wch:14},{wch:14},{wch:14},{wch:14},{wch:14},{wch:14},{wch:14},{wch:14},{wch:10}]; // Currency format on key columns of rollup for(let r=12;r<12+sums.length+1;r++){ ['D','E','F','G','H','I','J'].forEach(c=>{const a=c+(r+1);if(ws1[a])ws1[a].z='$#,##0.00';}); const ka='K'+(r+1);if(ws1[ka])ws1[ka].z='0.00%'; } // Currency format on portfolio totals (rows 6-10) and CIA split ['B6','B7','B8','B10'].forEach(a=>{if(ws1[a])ws1[a].z='$#,##0.00';}); if(ws1['B9'])ws1['B9'].z='0.00%'; XLSX.utils.book_append_sheet(wb,ws1,'Summary'); // ─── ONE TAB PER PROPERTY (Schedule E format) ─── PROPS.forEach(prop=>{ const ts=txns('ytd',prop.id);const r=rev(ts),e=exp(ts),n=r-e; if(r===0&&e===0)return; const rows=[ [`${prop.name} β€” Schedule E ${year}`], [prop.llc], [`Account ${prop.acct}${prop.partner?` Β· ${Math.round((prop.partner.split||0.5)*100)}/${100-Math.round((prop.partner.split||0.5)*100)} with ${prop.partner.name||'Partner'}`:''}`], [`Period: Full year ${year}`], [], ['IRS Line','Description','Amount','Notes'], ]; let totalExp=0; SCHE_LINES.forEach(L=>{ let amt=0; if(L.income){amt=r;} else if(L.cats&&L.cats.length){ ts.filter(t=>t.t==='expense'&&L.cats.includes(t.c)).forEach(t=>amt+=Math.abs(t.amount)); } if(!L.income)totalExp+=amt; rows.push([L.line,L.label,amt,'']); }); // Apportioned mileage for this property const propMile=mileDed/PROPS.length; rows.push(['Line 6','Auto and travel (mileage)',propMile,`Apportioned share of ${Math.round(totMi)} business mi`]); totalExp+=propMile; rows.push([]); rows.push(['','Total Expenses',totalExp,'']); rows.push(['','Net Income / (Loss)',n - propMile,'']); if(prop.partner){ const sp=prop.partner.split||0.5; const you=Math.round(sp*100); const them=100-you; const pn=prop.partner.name||'Partner'; const net=n-propMile; rows.push([]); rows.push(['',`Your ${you}% Share`,net*sp,'']); rows.push(['',`${pn}'s ${them}% Share`,net*(1-sp),'']); } rows.push([]); rows.push(['SUPPORTING DETAIL']); rows.push(['Date','Payee','Category','Sub-category','Amount','Source','IRS Line']); ts.slice().sort((a,b)=>new Date(a.date)-new Date(b.date)).forEach(t=>{ const lineMap={Insurance:'9',Cleaning:'7',Professional:'10',Management:'11',Mortgage:'12',Repairs:'14',Supplies:'15',Taxes:'16',Licenses:'16',Utilities:'17',Travel:'6',Meals:'19'}; rows.push([t.date,t.payee||t.desc,t.c,t.s||'',t.amount,t.src||'',t.c==='Revenue'?'3':(lineMap[t.c]||'19')]); }); const ws=XLSX.utils.aoa_to_sheet(rows); ws['!cols']=[{wch:10},{wch:36},{wch:16},{wch:18},{wch:14},{wch:12},{wch:10}]; // Apply currency to amount column on summary lines (rows 7..7+SCHE_LINES.length) for(let r2=6;r2<6+SCHE_LINES.length+3;r2++){const a='C'+(r2+1);if(ws[a])ws[a].z='$#,##0.00';} if(prop.partner){['C'+(6+SCHE_LINES.length+5),'C'+(6+SCHE_LINES.length+6)].forEach(a=>{if(ws[a])ws[a].z='$#,##0.00';});} // Currency on detail rows for(let key in ws){if(key[0]==='E'&&parseInt(key.slice(1))>SCHE_LINES.length+10)ws[key].z='$#,##0.00';} const tabName=prop.short+' Β· '+prop.name.slice(0,18); XLSX.utils.book_append_sheet(wb,ws,tabName.slice(0,31)); }); // ─── ALL TRANSACTIONS TAB ─── const txRows=[['Date','Property','LLC','Account','Payee','Description','Category','Sub','Type','Amount','Schedule E','Source','Verified βœ“']]; const allTx=[...D.transactions,...D.manual].filter(t=>t.t!=='transfer').sort((a,b)=>new Date(a.date)-new Date(b.date)); const lineMap={Insurance:'Line 9',Cleaning:'Line 7',Professional:'Line 10',Management:'Line 11',Mortgage:'Line 12',Repairs:'Line 14',Supplies:'Line 15',Taxes:'Line 16',Licenses:'Line 16',Utilities:'Line 17',Revenue:'Line 3',Travel:'Line 6',Meals:'Line 19'}; allTx.forEach(t=>{const p=PROPS.find(x=>x.id===t.pid);txRows.push([t.date,p?p.name:'β€”',p?p.llc:'',t.acct||'',t.payee||'',t.desc||'',t.c,t.s||'',t.t,t.amount,lineMap[t.c]||'Line 19',t.src||'',''])}); const ws3=XLSX.utils.aoa_to_sheet(txRows); ws3['!cols']=[{wch:11},{wch:22},{wch:28},{wch:10},{wch:30},{wch:30},{wch:14},{wch:18},{wch:9},{wch:14},{wch:11},{wch:11},{wch:11}]; ws3['!autofilter']={ref:'A1:M1'}; for(let i=2;i<=allTx.length+1;i++){const a='J'+i;if(ws3[a])ws3[a].z='$#,##0.00';} XLSX.utils.book_append_sheet(wb,ws3,'All Transactions'); // ─── MILEAGE TAB ─── if(D.miles.length){ const mRows=[['Date','From','To','Miles','Rate','Deduction','Vehicle','Purpose']]; D.miles.slice().sort((a,b)=>new Date(a.date)-new Date(b.date)).forEach(m=>{mRows.push([m.date,m.from||'',m.to||'',m.miles,mileRate,+((+m.miles||0)*mileRate).toFixed(2),m.vehicle||'',m.purpose||'Business'])}); mRows.push([]); mRows.push(['TOTAL','','',totMi,'',mileDed,'','']); const ws4=XLSX.utils.aoa_to_sheet(mRows); ws4['!cols']=[{wch:11},{wch:30},{wch:30},{wch:9},{wch:8},{wch:12},{wch:18},{wch:24}]; for(let i=2;i<=D.miles.length+3;i++){ const a='F'+i;if(ws4[a])ws4[a].z='$#,##0.00'; const b='E'+i;if(ws4[b])ws4[b].z='$0.000'; } XLSX.utils.book_append_sheet(wb,ws4,'Mileage'); } // ─── 1099 VENDOR TAB ─── const ven=vendor1099Data(); if(ven.length){ const venRows=[ ['1099-NEC VENDOR TRACKER'], [`Tax Year ${year} Β· Vendors paid $600+ likely require a 1099-NEC`], [`Excludes: utilities, mortgages, taxes, platforms (Airbnb/VRBO), and obvious corporations`], [], ['Vendor','Total Paid','# of Txns','Categories','Last Paid','1099 Required?','W-9 on file?','TIN','Address'], ]; ven.forEach(v=>{venRows.push([v.name,v.total,v.count,v.cats,v.lastDate||'',v.total>=600?'YES β€” file 1099-NEC':'No',' ',' ',' ']);}); const wsv=XLSX.utils.aoa_to_sheet(venRows); wsv['!cols']=[{wch:32},{wch:14},{wch:10},{wch:28},{wch:12},{wch:24},{wch:14},{wch:14},{wch:30}]; wsv['!autofilter']={ref:`A5:I${5+ven.length}`}; for(let i=6;i<=5+ven.length;i++){const a='B'+i;if(wsv[a])wsv[a].z='$#,##0.00';} XLSX.utils.book_append_sheet(wb,wsv,'1099 Vendors'); } // ─── COVER NOTE TAB ─── const cover=[ ['NOTES FOR ACCOUNTANT'], [], ['File','RentReel export'], ['Client',bizName()||'STR Portfolio'], ['Tax Year',year], ['Generated',new Date().toLocaleString()], ['Period covered','Full year (YTD)'], [], ['HOW TO USE THIS WORKBOOK'], ['1. Summary tab','Portfolio rollup + LLC breakdown + partner splits'], ['2. Per-property tabs','Each property has a Schedule E line breakdown and supporting transactions'], ['3. All Transactions','Full transaction log, filterable. Use the Verified column for tickmarks.'], ['4. Mileage','Business miles at the IRS rate Β· auto-deduction calc'], ['5. 1099 Vendors','Vendors paid $600+ this year. Collect W-9s and file 1099-NECs by Jan 31.'], [], ['BUSINESS STRUCTURE'], ['Business Name',bizName()||'(not set)'], ...LLCS.map((l,i)=>{ const ps=PROPS.filter(p=>l.props.includes(p.id)); return [`LLC ${i+1}`,`${l.name} β€” ${ps.map(p=>p.name).join(', ')}${l.partner?' (partnership)':''}`]; }), [], ['DATA SOURCES'], ['Reservations','PMS export (Hospitable, Guesty, Hostaway, OwnerRez, Lodgify, or Airbnb-direct)'], ['Cleaning','Cleaning platform / bank transactions'], ['Banks','Bank statement CSVs'], ['Cards','Credit card CSVs (business spend, contractors, software)'], ['Mileage','Mileage tracker CSV (MileIQ, Everlance, Stride, Hurdlr, TripLog, or spreadsheet)'], [], ['QUESTIONS'], ['Contact',cloud.user?cloud.user.email:'(sign in via Cloud Sync)'], ]; const ws0=XLSX.utils.aoa_to_sheet(cover); ws0['!cols']=[{wch:22},{wch:70}]; // Put cover first wb.SheetNames.unshift(wb.SheetNames.pop()); XLSX.utils.book_append_sheet(wb,ws0,'README'); // Move README to front const idx=wb.SheetNames.indexOf('README'); if(idx>0){wb.SheetNames.splice(idx,1);wb.SheetNames.unshift('README');} const bizSlug=(bizName()||'portfolio').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'').slice(0,50)||'portfolio'; XLSX.writeFile(wb,`schedule-e-${year}-${bizSlug}.xlsx`); toast('Excel workbook exported for CPA βœ“'); } function exportSche(){ if(!guardDemoAction('export your books'))return; if(!D.settings)D.settings={};D.settings.hasExported=true;save(); const Y=activeYear();const totMi=D.miles.reduce((s,m)=>s+m.miles,0);let txt=`SCHEDULE E β€” ${Y} TAX YEAR\n${bizName()||'STR Portfolio'}\n\n`; PROPS.forEach(prop=>{ const ts=txns('ytd',prop.id);const r=rev(ts),e=exp(ts),n=r-e;if(r===0&&e===0)return; txt+=`${prop.name} (${prop.llc}) Β· Account ${prop.acct}\n`; txt+=`Line 3 - Rents received: $${r.toFixed(2)}\n`; const cats={Insurance:'Line 9',Cleaning:'Line 10',Management:'Line 11',Professional:'Line 11',Mortgage:'Line 12',Repairs:'Line 14',Taxes:'Line 16',Licenses:'Line 16',Utilities:'Line 17'}; Object.entries(cats).forEach(([c,l])=>{const a=Math.abs(ts.filter(t=>t.t==='expense'&&t.c===c).reduce((s,t)=>s+t.amount,0));if(a>0)txt+=`${l} - ${c}: $${a.toFixed(2)}\n`;}); txt+=`Net Income/(Loss): $${n.toFixed(2)}\n`; if(prop.partner)txt+=`Your 50% share: $${(n/2).toFixed(2)}\n`; txt+='\n'; }); if(totMi>0)txt+=`Vehicle Mileage: ${Math.round(totMi)} miles Γ— $0.725 = $${(totMi*0.725).toFixed(2)}\n`; dl(txt,`schedule-e-${Y}.txt`,'text/plain');toast('Schedule E exported βœ“'); } function exportMiles(){ if(!guardDemoAction('export mileage records'))return; const rate=mileRate();const Y=String(activeYear()); const rows=(D.miles||[]).filter(m=>(m.date||'').slice(0,4)===Y).sort((a,b)=>(a.date||'').localeCompare(b.date||'')); let csv='DATE,FROM,TO,MILES,DEDUCTION,VEHICLE,PURPOSE\n'; rows.forEach(m=>csv+=`"${m.date}","${(m.from||'').replace(/"/g,'')}","${(m.to||'').replace(/"/g,'')}",${(+m.miles||0).toFixed(1)},${((+m.miles||0)*rate).toFixed(2)},"${m.vehicle||''}","${(m.purpose||'').replace(/"/g,'')}"\n`); const t=rows.reduce((s,m)=>s+(+m.miles||0),0);csv+=`\nTOTAL,,,${t.toFixed(1)},${(t*rate).toFixed(2)},,\nIRS rate,,,$${rate.toFixed(3)}/mi,,,\n`; dl(csv,`mileage-${Y}.csv`);toast(`Mileage ${Y} exported βœ“`); } function exportSplit(){ if(!guardDemoAction('export partner split reports'))return; const partnerProps=PROPS.filter(p=>p.partner); if(!partnerProps.length){toast('No partnership properties to export');return;} let txt=`PARTNER SPLIT REPORT β€” ${bizName()||'Rental Portfolio'}\nGenerated ${new Date().toLocaleDateString()}\n\n`; partnerProps.forEach(prop=>{ const ts=txns('ytd',prop.id);const r=rev(ts),e=exp(ts),n=r-e; const yourPct=Math.round((prop.partner.split||0.5)*100); const partnerPct=100-yourPct; const yourShare=n*(prop.partner.split||0.5); const partnerShare=n-yourShare; const partnerName=prop.partner.name||'Partner'; txt+=`${prop.name} Β· ${prop.llc} Β· ${yourPct}/${partnerPct} with ${partnerName}\n`; txt+=`Revenue: $${r.toFixed(2)}\nExpenses: $${e.toFixed(2)}\nNet: $${n.toFixed(2)}\n\n`; txt+=`YOUR SHARE (${yourPct}%): $${yourShare.toFixed(2)}\n${partnerName.toUpperCase()}'S SHARE (${partnerPct}%): $${partnerShare.toFixed(2)}\n\n${'─'.repeat(50)}\n\n`; }); dl(txt,'partner-split.txt','text/plain');toast('Split exported βœ“'); } function clearAll(){ if(!confirm('Clear all uploaded data? Cannot be undone.'))return; D.transactions=[];D.miles=[];D.manual=[];D.uploads=[];D.lastUp=null; D.demoSeeded=true; // prevent re-seed on next load (and tells cloud this is intentional empty) save();rUpload();rDash();toast('All data cleared'); } // ═══ INIT ═══ function seedDemo(force){ // Don't auto-seed if user has already seen demo (or explicitly cleared) if(!force&&D.demoSeeded)return; if(D.transactions.length > 0) return; // don't overwrite real data // Synthetic sample portfolio β€” no real vendor names, no real accounts, no real amounts. // Property IDs p1-p5 map to DEFAULT_PROPS (Sunset Cove Cabin, Ridgeview Retreat, etc.) const hospData = [ {date:'2026-01-04',payee:'airbnb',desc:'Airbnb HMR1001 5n',amount:680.00,pid:'p1',s:'Airbnb'}, {date:'2026-01-03',payee:'airbnb',desc:'Airbnb HMR1002 3n',amount:550.00,pid:'p1',s:'Airbnb'}, {date:'2026-01-11',payee:'airbnb',desc:'Airbnb HMR1003 7n',amount:920.00,pid:'p3',s:'Airbnb'}, {date:'2026-01-04',payee:'airbnb',desc:'Airbnb HMR1004 4n',amount:2800.00,pid:'p4',s:'Airbnb'}, {date:'2026-01-18',payee:'airbnb',desc:'Airbnb HMR1005 6n',amount:2900.00,pid:'p4',s:'Airbnb'}, {date:'2026-01-10',payee:'airbnb',desc:'Airbnb HMR1006 4n',amount:1520.00,pid:'p5',s:'Airbnb'}, {date:'2026-01-24',payee:'airbnb',desc:'Airbnb HMR1007 3n',amount:1230.00,pid:'p5',s:'Airbnb'}, {date:'2026-02-08',payee:'airbnb',desc:'Airbnb HMR1008 2n',amount:460.00,pid:'p1',s:'Airbnb'}, {date:'2026-02-14',payee:'airbnb',desc:'Airbnb HMR1009 5n',amount:2650.00,pid:'p4',s:'Airbnb'}, {date:'2026-02-11',payee:'vrbo',desc:'VRBO HMR1010 4n',amount:1520.00,pid:'p2',s:'VRBO'}, {date:'2026-03-06',payee:'airbnb',desc:'Airbnb HMR1011 4n',amount:1140.00,pid:'p4',s:'Airbnb'}, {date:'2026-03-14',payee:'airbnb',desc:'Airbnb HMR1012 3n',amount:1520.00,pid:'p1',s:'Airbnb'}, {date:'2026-03-20',payee:'airbnb',desc:'Airbnb HMR1013 5n',amount:2080.00,pid:'p2',s:'Airbnb'}, {date:'2026-03-22',payee:'airbnb',desc:'Airbnb HMR1014 4n',amount:1210.00,pid:'p5',s:'Airbnb'}, {date:'2026-04-01',payee:'airbnb',desc:'Airbnb HMR1015 3n',amount:1080.00,pid:'p2',s:'Airbnb'}, {date:'2026-04-10',payee:'airbnb',desc:'Airbnb HMR1016 8n',amount:3380.00,pid:'p4',s:'Airbnb'}, {date:'2026-04-14',payee:'airbnb',desc:'Airbnb HMR1017 4n',amount:510.00,pid:'p2',s:'Airbnb'}, {date:'2026-04-18',payee:'airbnb',desc:'Airbnb HMR1018 4n',amount:1560.00,pid:'p1',s:'Airbnb'}, {date:'2026-04-20',payee:'airbnb',desc:'Airbnb HMR1019 5n',amount:600.00,pid:'p2',s:'Airbnb'}, {date:'2026-04-21',payee:'airbnb',desc:'Airbnb HMR1020 5n',amount:1520.00,pid:'p1',s:'Airbnb'}, {date:'2026-04-24',payee:'airbnb',desc:'Airbnb HMR1021 4n',amount:1220.00,pid:'p5',s:'Airbnb'}, {date:'2026-04-28',payee:'airbnb',desc:'Airbnb HMR1022 2n',amount:780.00,pid:'p4',s:'Airbnb'}, {date:'2026-04-28',payee:'vrbo',desc:'VRBO HMR1023 2n',amount:370.00,pid:'p3',s:'VRBO'}, {date:'2026-04-30',payee:'airbnb',desc:'Airbnb HMR1024 3n',amount:1390.00,pid:'p4',s:'Airbnb'}, {date:'2026-05-08',payee:'airbnb',desc:'Airbnb HMR1025 2n',amount:460.00,pid:'p3',s:'Airbnb'}, {date:'2026-05-14',payee:'airbnb',desc:'Airbnb HMR1026 5n',amount:2080.00,pid:'p2',s:'Airbnb'}, {date:'2026-05-20',payee:'airbnb',desc:'Airbnb HMR1027 5n',amount:1130.00,pid:'p5',s:'Airbnb'}, {date:'2026-06-17',payee:'vrbo',desc:'VRBO HMR1028 6n',amount:2080.00,pid:'p4',s:'VRBO'}, {date:'2026-06-20',payee:'airbnb',desc:'Airbnb HMR1029 5n',amount:2350.00,pid:'p1',s:'Airbnb'}, {date:'2026-06-22',payee:'airbnb',desc:'Airbnb HMR1030 6n',amount:2550.00,pid:'p2',s:'Airbnb'}, ]; hospData.forEach(d=>{ const prop=PROPS.find(p=>p.id===d.pid); D.transactions.push({id:uid(),date:d.date,payee:d.payee,desc:d.desc,amount:d.amount,acct:prop?.acct||'',pid:d.pid,llc:prop?.llc||null,c:'Revenue',s:d.s,t:'income',se:null,src:'hospitable',ok:true,ex:{nights:parseInt(d.desc.match(/(\d+)n/)?.[1]||'3'),base:d.amount,plat:d.payee}}); }); // Synthetic expenses using generic vendor names + demo account numbers matching DEFAULT_PROPS. const expenses = [ // Mortgages {date:'2026-01-02',payee:'ROCKET MORTGAGE',amount:-2075.00,pid:'p1',c:'Mortgage',s:'Mortgage',acct:'1234'}, {date:'2026-02-02',payee:'ROCKET MORTGAGE',amount:-2075.00,pid:'p1',c:'Mortgage',s:'Mortgage',acct:'1234'}, {date:'2026-03-02',payee:'ROCKET MORTGAGE',amount:-2075.00,pid:'p1',c:'Mortgage',s:'Mortgage',acct:'1234'}, {date:'2026-04-01',payee:'ROCKET MORTGAGE',amount:-2123.00,pid:'p1',c:'Mortgage',s:'Mortgage',acct:'1234'}, {date:'2026-01-01',payee:'MR COOPER LOAN SVC',amount:-2630.00,pid:'p2',c:'Mortgage',s:'Mortgage',acct:'2345'}, {date:'2026-02-01',payee:'MR COOPER LOAN SVC',amount:-2630.00,pid:'p2',c:'Mortgage',s:'Mortgage',acct:'2345'}, {date:'2026-03-01',payee:'MR COOPER LOAN SVC',amount:-2630.00,pid:'p2',c:'Mortgage',s:'Mortgage',acct:'2345'}, {date:'2026-04-01',payee:'MR COOPER LOAN SVC',amount:-2630.00,pid:'p2',c:'Mortgage',s:'Mortgage',acct:'2345'}, {date:'2026-04-08',payee:'MR COOPER LOAN SVC',amount:-1685.00,pid:'p5',c:'Mortgage',s:'Mortgage',acct:'5678'}, {date:'2026-04-12',payee:'WELLS FARGO HOME MTG',amount:-866.00,pid:'p3',c:'Mortgage',s:'Mortgage',acct:'3456'}, {date:'2026-04-02',payee:'SUNRUN SOLAR LOAN',amount:-117.00,pid:'p4',c:'Mortgage',s:'Solar Loan',acct:'4567'}, {date:'2026-04-07',payee:'CHASE HOME LENDING',amount:-980.00,pid:'p4',c:'Mortgage',s:'Mortgage/Loan',acct:'4567'}, {date:'2026-04-21',payee:'CHASE HOME LENDING',amount:-980.00,pid:'p4',c:'Mortgage',s:'Mortgage/Loan',acct:'4567'}, // Utilities {date:'2026-03-13',payee:'DUKE ENERGY',amount:-560.00,pid:'p1',c:'Utilities',s:'Electric',acct:'1234'}, {date:'2026-04-21',payee:'DUKE ENERGY',amount:-131.00,pid:'p4',c:'Utilities',s:'Electric',acct:'4567'}, {date:'2026-04-06',payee:'DUKE ENERGY',amount:-82.00,pid:'p3',c:'Utilities',s:'Electric',acct:'3456'}, {date:'2026-04-06',payee:'FPL',amount:-96.00,pid:'p5',c:'Utilities',s:'Electric',acct:'5678'}, {date:'2026-04-27',payee:'COLUMBIA GAS',amount:-116.00,pid:'p4',c:'Utilities',s:'Gas',acct:'4567'}, {date:'2026-04-14',payee:'COLUMBIA GAS',amount:-72.00,pid:'p3',c:'Utilities',s:'Gas',acct:'3456'}, {date:'2026-03-04',payee:'CITY OF SUNSET WATER DEPT',amount:-53.00,pid:'p1',c:'Utilities',s:'Water',acct:'1234'}, {date:'2026-04-15',payee:'CITY OF PALM GROVE WATER',amount:-27.00,pid:'p5',c:'Utilities',s:'Water/Sewer',acct:'5678'}, {date:'2026-04-15',payee:'SPECTRUM',amount:-89.99,pid:'p1',c:'Utilities',s:'Internet',acct:'1234'}, {date:'2026-04-16',payee:'SPECTRUM',amount:-89.99,pid:'p2',c:'Utilities',s:'Internet',acct:'2345'}, // Cleaning {date:'2026-05-22',payee:'MERRY MAIDS OF AUSTIN',amount:-259.00,pid:'p1',c:'Cleaning',s:'Cleaning Service',acct:'1234'}, {date:'2026-05-25',payee:'MERRY MAIDS OF AUSTIN',amount:-259.00,pid:'p2',c:'Cleaning',s:'Cleaning Service',acct:'2345'}, {date:'2026-05-25',payee:'SPARKLE CLEAN CO',amount:-151.00,pid:'p4',c:'Cleaning',s:'Cleaning Service',acct:'4567'}, {date:'2026-04-02',payee:'BEACHSIDE HOUSEKEEPING',amount:-1277.00,pid:'p5',c:'Cleaning',s:'Cleaning Service',acct:'5678'}, // Repairs {date:'2026-05-10',payee:'HANDY SOLUTIONS LLC',amount:-5200.00,pid:'p4',c:'Repairs',s:'Contractor',acct:'cc'}, {date:'2026-05-17',payee:'HANDY SOLUTIONS LLC',amount:-1190.00,pid:'p4',c:'Repairs',s:'Contractor',acct:'cc'}, // Software / Management {date:'2026-04-19',payee:'HOSPITABLE, INC',amount:-245.00,pid:null,c:'Management',s:'PMS Software',acct:'cc'}, {date:'2026-04-29',payee:'PRICELABS',amount:-526.00,pid:null,c:'Management',s:'Dynamic Pricing',acct:'cc'}, {date:'2026-05-01',payee:'GOOGLE WORKSPACE',amount:-36.00,pid:null,c:'Management',s:'Google Workspace',acct:'cc'}, // Professional {date:'2026-04-22',payee:'SMITH TAX ADVISORY LLC',amount:-240.00,pid:null,c:'Professional',s:'Accountant',acct:'cc'}, // Insurance {date:'2026-04-12',payee:'STATE FARM',amount:-54.00,pid:'p5',c:'Insurance',s:'Insurance',acct:'5678'}, {date:'2026-04-08',payee:'PROPER INSURANCE',amount:-152.00,pid:'p1',c:'Insurance',s:'STR Insurance',acct:'1234'}, // Taxes {date:'2026-04-05',payee:'COUNTY TAX ASSESSOR',amount:-2023.00,pid:'p4',c:'Taxes',s:'Property Tax',acct:'4567'}, {date:'2026-04-05',payee:'COUNTY TAX ASSESSOR',amount:-734.00,pid:'p3',c:'Taxes',s:'Property Tax',acct:'3456'}, ]; expenses.forEach(e=>{ const prop=PROPS.find(p=>p.id===e.pid); D.transactions.push({id:uid(),date:e.date,payee:e.payee,desc:e.payee,amount:e.amount,acct:e.acct,pid:e.pid,llc:prop?.llc||null,c:e.c,s:e.s,t:'expense',se:null,src:e.acct==='cc'?'card':'bank',ok:true}); }); // Synthetic mileage with generic locations + a demo vehicle name. const milesData = [ {date:'2026-04-04',from:'Home Base',to:'Sunset Cove Cabin',miles:22.0,purpose:'Property Inspection'}, {date:'2026-04-06',from:'Sunset Cove Cabin',to:'Home Base',miles:22.0,purpose:'Property Inspection'}, {date:'2026-04-12',from:'Home Base',to:'Ridgeview Retreat',miles:142.0,purpose:'STR Property Visit'}, {date:'2026-04-14',from:'Ridgeview Retreat',to:'Home Base',miles:142.0,purpose:'STR Property Visit'}, {date:'2026-04-18',from:'Home Base',to:'Palm Grove Villa',miles:138.0,purpose:'STR Property Visit'}, {date:'2026-04-20',from:'Palm Grove Villa',to:'Home Base',miles:138.0,purpose:'STR Property Visit'}, ]; milesData.forEach(m=>D.miles.push({id:uid(),date:m.date,from:m.from,to:m.to,miles:m.miles,vehicle:'Business Vehicle',purpose:m.purpose,ded:parseFloat((m.miles*0.725).toFixed(2))})); D.lastUp=Date.now(); D.demoSeeded=true; save(); } // Note: seedDemo() is NOT auto-called anymore. New users see auth landing β†’ Welcome state. // "Load Sample Portfolio" button in Welcome state triggers reseedDemo() if they want demo data. applyTheme(D.settings.theme||'light'); applyBizName(); if(reconcilePlatformPayouts()>0)save_local(); // fix any double-counted deposits from prior versions updTs();rDash();rUpload(); // If this URL is a CPA share link (#cpa=...) OR a CPA demo (?cpa-demo=1), short-circuit and render read-only view. if(!checkCpaHash() && !checkCpaDemo()){ cloud.init(); // Trigger onboarding tour on first-ever visit (once user is signed in, if not already seen). setTimeout(()=>{if(!D.tourSeen&&typeof cloud!=='undefined'&&cloud.user&&!D.transactions.length&&!(D.uploads&&D.uploads.length))openOnboardingTour();},2500); // Reveal the Founder Console nav item once we know the signed-in email. setTimeout(ensureFounderNav,1500); maybeShowAuthLanding(); } // ═══ COMMAND PALETTE (⌘K) ═══ // Global keyboard shortcut opens a searchable palette β€” jump to any page, property, or transaction. document.addEventListener('keydown',e=>{ if((e.metaKey||e.ctrlKey)&&e.key.toLowerCase()==='k'){e.preventDefault();openCmdPalette();} if(e.key==='Escape'){const m=document.getElementById('cmd-modal');if(m&&m.style.display==='flex')closeCmdPalette();} });