🏒 Welcome to HOPE Apartments

Find available beds, view building and flat information, review basic room rules, and send a rental request directly to HOPE Apartments management.

πŸ›οΈ Available Rooms & Beds

Loading availability…

🏒 Available Flats & Building Information

Loading property information…

πŸ’¬ Contact HOPE Apartments

Have a question or want to ask about an available room or flat?

πŸ’¬ Contact Us Through WhatsApp

WhatsApp contact will be available when management publishes its number.

πŸ“‹ Basic Room Rules

  • Respect other tenants and keep noise under control.
  • Keep bedrooms, kitchen and shared areas clean.
  • No unauthorized occupants or subletting.
  • Pay rent on the agreed date.
  • Report maintenance problems to management promptly.
  • Follow building safety and community requirements.

πŸ“ Request an Available Bed

Send your details to management. A request does not confirm a booking until management approves it.

`); w.document.close();w.focus();w.print(); } function exportManagementCSV(){ generateManagementReport(); const from=reportDateValue('mgFrom',''),to=reportDateValue('mgTo',''),pid=document.getElementById('mgProperty')?.value||''; const props=(db.properties||[]).filter(p=>!pid||p.id===pid); const lines=[['Property','Unit','Bed','Tenant','Monthly Rent','Payments in Period','Status'].join(',')]; props.forEach(p=>(p.beds||[]).forEach(b=>{ if(!b.tenant)return; const paid=db.payments.filter(x=>x.property===p.id&&String(x.bed)===String(b.no)&&inDateRange(x.date,from,to)).reduce((a,x)=>a+(+x.amount||0),0); const due=+b.rent||+p.target||0; lines.push([p.name,p.unit,b.no,b.tenant,due,paid,paid>=due?'Paid':paid>0?'Partial':'Pending'].map(v=>`"${String(v??'').replace(/"/g,'""')}"`).join(',')); })); const blob=new Blob([lines.join('\n')],{type:'text/csv;charset=utf-8'}); const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='AGBM_Management_Report_'+today+'.csv';a.click();setTimeout(()=>URL.revokeObjectURL(a.href),500); } function reportDateValue(id, fallback){ const el=document.getElementById(id); return el&&el.value?el.value:fallback; } function populateManagementReportFilters(){ const sel=document.getElementById('mgProperty'); if(!sel)return; const old=sel.value; sel.innerHTML=''+(db.properties||[]).map(p=>``).join(''); if(old)sel.value=old; const now=today.slice(0,7); const from=document.getElementById('mgFrom'),to=document.getElementById('mgTo'); if(from&&!from.value)from.value=now+'-01'; if(to&&!to.value)to.value=today; } function inDateRange(date,from,to){ const d=String(date||''); return (!from||d>=from)&&(!to||d<=to); } function generateManagementReport(){ if(!document.getElementById('managementReports'))return; populateManagementReportFilters(); const from=reportDateValue('mgFrom','0000-00-00'),to=reportDateValue('mgTo','9999-99-99'); const pid=document.getElementById('mgProperty')?.value||''; const props=(db.properties||[]).filter(p=>!pid||p.id===pid); let totalBeds=0,occupied=0,rentDue=0,collected=0,expenses=0; const rows=[]; props.forEach(p=>{ const beds=p.beds||[]; const occ=beds.filter(b=>b.tenant); totalBeds+=beds.length;occupied+=occ.length; let due=0,paid=0,exp=0; occ.forEach(b=>{ const monthly=+b.rent||+p.target||0; // Calculate one monthly due for each calendar month touched by the selected period. let start=new Date((from==='0000-00-00'?today:from)+'T00:00:00'); let end=new Date((to==='9999-99-99'?today:to)+'T00:00:00'); if(isNaN(start)||isNaN(end)){start=new Date();end=new Date();} let cursor=new Date(start.getFullYear(),start.getMonth(),1); const last=new Date(end.getFullYear(),end.getMonth(),1); let months=0; while(cursor<=last && months<120){months++;cursor.setMonth(cursor.getMonth()+1);} due+=monthly*months; paid+=db.payments.filter(x=>x.property===p.id&&String(x.bed)===String(b.no)&&inDateRange(x.date,from,to)).reduce((a,x)=>a+(+x.amount||0),0); }); exp=db.expenses.filter(x=>x.property===p.id&&inDateRange(x.date,from,to)).reduce((a,x)=>a+(+x.amount||0),0); rentDue+=due;collected+=paid;expenses+=exp; rows.push({p,occ:occ.length,vacant:beds.length-occ.length,due,paid,balance:Math.max(0,due-paid),exp,net:paid-exp}); }); const outstanding=Math.max(0,rentDue-collected); const occRate=totalBeds?occupied/totalBeds*100:0; const collectionRate=rentDue?collected/rentDue*100:0; const net=collected-expenses; document.getElementById('mgMetrics').innerHTML=[ ['Total Beds',totalBeds,''], ['Occupied',occupied,'green'], ['Available',Math.max(0,totalBeds-occupied),''], ['Rent Due',money(rentDue),''], ['Collected',money(collected),'green'], ['Outstanding',money(outstanding),outstanding?'red':'green'], ['Expenses',money(expenses),''], ['Net Cash Result',money(net),net>=0?'green':'red'] ].map(x=>`
${x[0]}
${typeof x[1]==='number'&&x[0].includes('Beds')||x[0]==='Occupied'||x[0]==='Available'?x[1]:x[1]}
`).join(''); document.getElementById('mgOccupancy').innerHTML= `
${occRate.toFixed(1)}%

${occupied} occupied of ${totalBeds} total beds.

`; document.getElementById('mgCollection').innerHTML= `
${collectionRate.toFixed(1)}%

${money(collected)} collected against ${money(rentDue)} rent due.

`; document.getElementById('mgPropertyTable').innerHTML=`${ rows.map(r=>``).join('') }
PropertyOccupiedAvailableRent DueCollectedOutstandingExpensesNet
${esc(r.p.name)} / ${esc(r.p.unit)}${r.occ}${r.vacant}${money(r.due)}${money(r.paid)}${money(r.balance)}${money(r.exp)}${money(r.net)}
`; const outstandingRows=[]; rows.forEach(r=>(r.p.beds||[]).forEach(b=>{ if(!b.tenant)return; const due=+b.rent||+r.p.target||0; const paid=db.payments.filter(x=>x.property===r.p.id&&String(x.bed)===String(b.no)&&inDateRange(x.date,from,to)).reduce((a,x)=>a+(+x.amount||0),0); // For the selected period, approximate due using the number of months represented by the filter. let start=new Date(from+'T00:00:00'),end=new Date(to+'T00:00:00'); let months=1; if(!isNaN(start)&&!isNaN(end)) months=Math.max(1,(end.getFullYear()-start.getFullYear())*12+end.getMonth()-start.getMonth()+1); const bal=Math.max(0,due*months-paid); if(bal>0)outstandingRows.push({p:r.p,b,due:due*months,paid,bal}); })); document.getElementById('mgOutstanding').innerHTML=outstandingRows.length?`${outstandingRows.map(x=>``).join('')}
TenantPropertyBedDuePaidBalance
${esc(x.b.tenant)}${esc(x.p.name)} / ${esc(x.p.unit)}${esc(x.b.no)}${money(x.due)}${money(x.paid)}${money(x.bal)}
`:'
No outstanding tenant balances were found for the selected period.
'; const expenseMap={}; db.expenses.filter(x=>(!pid||x.property===pid)&&inDateRange(x.date,from,to)).forEach(x=>{expenseMap[x.type]=(expenseMap[x.type]||0)+(+x.amount||0)}); const expenseEntries=Object.entries(expenseMap).sort((a,b)=>b[1]-a[1]); document.getElementById('mgExpenses').innerHTML=expenseEntries.length?`${expenseEntries.map(x=>``).join('')}
Expense TypeAmount
${esc(x[0])}${money(x[1])}
`:'
No expenses found for the selected period.
'; } function printManagementReport(){ generateManagementReport(); const v=document.getElementById('managementReports').innerHTML; const w=window.open('','_blank'); if(!w)return alert('Please allow pop-ups to print the report.'); w.document.write(`AGBM Management Report${css}${v}`); w.document.close();w.focus();w.print(); } function exportManagementCSV(){ generateManagementReport(); const from=reportDateValue('mgFrom',''),to=reportDateValue('mgTo',''),pid=document.getElementById('mgProperty')?.value||''; const props=(db.properties||[]).filter(p=>!pid||p.id===pid); const lines=[['Property','Unit','Bed','Tenant','Monthly Rent','Payments in Period','Status'].join(',')]; props.forEach(p=>(p.beds||[]).forEach(b=>{ if(!b.tenant)return; const paid=db.payments.filter(x=>x.property===p.id&&String(x.bed)===String(b.no)&&inDateRange(x.date,from,to)).reduce((a,x)=>a+(+x.amount||0),0); const due=+b.rent||+p.target||0; lines.push([p.name,p.unit,b.no,b.tenant,due,paid,paid>=due?'Paid':paid>0?'Partial':'Pending'].map(v=>`"${String(v??'').replace(/"/g,'""')}"`).join(',')); })); const blob=new Blob([lines.join('\n')],{type:'text/csv;charset=utf-8'}); const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='AGBM_Management_Report_'+today+'.csv';a.click();setTimeout(()=>URL.revokeObjectURL(a.href),500); } function renderReports(){let t=totals(),net=t.income-t.expense;document.getElementById('reportMetrics').innerHTML=[['Total Rent Income',t.income],['Total Building Expenses',t.expense],['Net Profit / Loss',net]].map(x=>`
${x[0]}
${money(x[1])}
`).join('');let h=[];h.push(net<0?'πŸ”΄ Overall property operation is loss-making.':'🟒 Overall property operation is profitable.');db.properties.forEach(p=>{let r=db.payments.filter(x=>x.property===p.id).reduce((a,x)=>a+(+x.amount||0),0),e=db.expenses.filter(x=>x.property===p.id).reduce((a,x)=>a+(+x.amount||0),0);if(r-e<0)h.push(`πŸ”΄ ${esc(p.name)} / ${esc(p.unit)} is loss-making.`)});let vac=db.properties.reduce((a,p)=>a+p.beds.filter(b=>!b.tenant).length,0);if(vac)h.push(`🟑 ${vac} bed(s) are vacant.`);let pend=db.properties.reduce((a,p)=>a+p.beds.filter(b=>b.tenant).filter(b=>{let due=+b.rent||+p.target||0;let paid=db.payments.filter(x=>x.property===p.id&&x.bed===b.no&&monthKey(x.date)===monthKey(today)).reduce((z,x)=>z+(+x.amount||0),0);return paid`
${x}
`).join('');document.getElementById('reportTable').innerHTML=`${db.properties.map(p=>{let r=db.payments.filter(x=>x.property===p.id).reduce((a,x)=>a+(+x.amount||0),0),e=db.expenses.filter(x=>x.property===p.id).reduce((a,x)=>a+(+x.amount||0),0);return``}).join('')}
PropertyOccupiedVacantIncomeExpensesProfit/Loss
${esc(p.name)} / ${esc(p.unit)}${p.beds.filter(b=>b.tenant).length}${p.beds.filter(b=>!b.tenant).length}${money(r)}${money(e)}${money(r-e)}
`} function renderLedger(){let m=ledgerMonth.value||monthKey(today),pid=ledgerProp.value||'';let rows=monthlyRows(m,pid),due=rows.reduce((a,x)=>a+x.due,0),paid=rows.reduce((a,x)=>a+x.paid,0),bal=rows.reduce((a,x)=>a+x.balance,0);document.getElementById('ledgerSummary').innerHTML=`
${monthName(m)} β€” Rent Due
${money(due)}
Collected
${money(paid)}
Outstanding
${money(bal)}
Occupants Billed
${rows.length}
`;document.getElementById('ledgerTable').innerHTML=rows.length?`${rows.map(x=>``).join('')}
PropertyBedTenantRent DuePaidBalanceStatus
${esc(x.p.name)} / ${esc(x.p.unit)}${x.b.no}${esc(x.b.tenant)}${money(x.due)}${money(x.paid)}${money(x.balance)}${x.status}
`:'
No occupied beds for this month/property.
'} function printLedger(){let old=document.body.innerHTML;let content=document.getElementById('ledger').innerHTML;document.body.innerHTML='

AGBM β€” Monthly Rent Ledger

'+content+'
';window.print();location.reload()} function renderBackup(){let bytes=new Blob([JSON.stringify(db)]).size;document.getElementById('dataStatus').innerHTML=`

Properties: ${db.properties.length}

Payment records: ${db.payments.length}

Expense records: ${db.expenses.length}

Approx. local data size: ${(bytes/1024).toFixed(1)} KB

Current version stores data locally on this device. Export backups regularly until cloud sync is added.

`} function selectors(){let opts=db.properties.map(p=>``).join('');[tenProp,payProp,exProp,transferProp].forEach(s=>{if(!s)return;let old=s.value;s.innerHTML=opts;if([...s.options].some(o=>o.value===old))s.value=old});let lo=ledgerProp.value;ledgerProp.innerHTML=''+opts;if([...ledgerProp.options].some(o=>o.value===lo))ledgerProp.value=lo;let p=db.properties.find(x=>x.id===tenProp.value);tenBed.innerHTML=p?p.beds.map(b=>``).join():'';let tp=db.properties.find(x=>x.id===transferProp.value);transferBed.innerHTML=tp?tp.beds.map(b=>``).join():'';p=db.properties.find(x=>x.id===payProp.value);payBed.innerHTML=p?p.beds.map(b=>``).join():'';loadTenant();loadPay();renderPropertyManager()} ledgerMonth.value=monthKey(today);[tenProp,payProp,transferProp].forEach(s=>s.addEventListener('change',selectors));ledgerMonth.addEventListener('change',renderLedger);ledgerProp.addEventListener('change',renderLedger);tenBed.addEventListener('change',loadTenant);payBed.addEventListener('change',loadPay);payDate.value=today;exDate.value=today; function savePublicContactSettings(){if(!requireAdmin())return;const phone=(document.getElementById('publicPhoneSetting').value||'').trim(),wa=(document.getElementById('publicWhatsAppSetting').value||'').trim();localStorage.setItem('agbm_public_phone',phone);localStorage.setItem('agbm_public_whatsapp',wa||phone);renderTenantHome();alert('Public contact settings saved.')}function loadPublicContactSettings(){const p=document.getElementById('publicPhoneSetting'),w=document.getElementById('publicWhatsAppSetting');if(p)p.value=localStorage.getItem('agbm_public_phone')||'';if(w)w.value=localStorage.getItem('agbm_public_whatsapp')||''} function initSecurity(){ document.getElementById('authGate').style.display='none'; document.getElementById('adminShell').style.display='none'; showTenantHome(); } initSecurity(); loadLocalDatabase(); updateDBStatus(); const CLOUD_KEY='agbm_v6_cloud'; let cloudCfg=JSON.parse(localStorage.getItem(CLOUD_KEY)||'null'); function saveCloudConfig(){ if(sessionRole!=='admin') return alert('Administrator access required.'); const url=(document.getElementById('cloudUrl').value||'').trim().replace(/\/+$/,''); const key=(document.getElementById('cloudKey').value||'').trim(); if(!/^https?:\/\//i.test(url)) return alert('Enter a valid HTTPS/HTTP project URL.'); if(!key) return alert('Enter the public project key.'); cloudCfg={url,key}; localStorage.setItem(CLOUD_KEY,JSON.stringify(cloudCfg)); updateCloudUI(); alert('Cloud settings saved on this device.'); } function updateCloudUI(){ const u=document.getElementById('cloudUrl'),k=document.getElementById('cloudKey'); if(u&&cloudCfg){u.value=cloudCfg.url||'';k.value=cloudCfg.key||'';} const s=document.getElementById('cloudStatus'); if(s) s.textContent=cloudCfg ? 'Cloud configuration is saved. Connection is ready for a supported backend.' : 'No cloud backend configured yet.'; const lb=document.getElementById('lastBackup'); if(lb) lb.textContent=localStorage.getItem('agbm_v6_last_backup')||'Never'; } async function testCloud(){ if(sessionRole!=='admin') return alert('Administrator access required.'); if(!cloudCfg) return alert('Save cloud settings first.'); try{ const r=await fetch(cloudCfg.url,{method:'GET',headers:{'apikey':cloudCfg.key}}); document.getElementById('cloudStatus').textContent='Connection reached the configured server (HTTP '+r.status+').'; }catch(e){ document.getElementById('cloudStatus').textContent='Connection test failed. Check the URL, internet connection, and backend configuration.'; } } function buildBackup(){ return {format:'AGBM',version:6,exportedAt:new Date().toISOString(),data:db}; } function cloudBackup(){ if(sessionRole!=='admin') return alert('Administrator access required.'); // Safe local preparation until a real backend endpoint is configured. const payload=JSON.stringify(buildBackup()); localStorage.setItem('agbm_v6_pending_cloud_backup',payload); localStorage.setItem('agbm_v6_last_backup',new Date().toLocaleString()); updateCloudUI(); alert('A complete backup snapshot has been prepared and saved locally. A real cloud backend must be connected to upload it off-device.'); } function restoreCloud(){ if(sessionRole!=='admin') return alert('Administrator access required.'); const raw=localStorage.getItem('agbm_v6_pending_cloud_backup'); if(!raw) return alert('No cloud backup snapshot is available on this device.'); if(!confirm('Restore the saved backup snapshot? Current local data will be replaced.')) return; const p=JSON.parse(raw); if(p?.data){db=p.data;localStorage.setItem(KEY,JSON.stringify(db));render();alert('Backup restored successfully.');} } updateCloudUI(); function populateTenantReportSelectors(){ const beds=document.getElementById('reportBed'), pays=document.getElementById('receiptPayment'); if(!beds||!pays)return; beds.innerHTML=''; (db.properties||[]).forEach(p=>{for(let i=1;i<=Number(p.beds||0);i++){let key=p.id+'|'+i;let t=(db.tenants||[]).find(x=>x.propertyId==p.id&&String(x.bed)==String(i));if(t) beds.innerHTML+=``;}}); pays.innerHTML=''; (db.payments||[]).slice().reverse().forEach((x,n)=>{let p=(db.properties||[]).find(q=>q.id==x.propertyId);let t=(db.tenants||[]).find(q=>q.propertyId==x.propertyId&&String(q.bed)==String(x.bed));if(t)pays.innerHTML+=``;}); } function fillReportTenant(){const v=document.getElementById('reportBed').value;const el=document.getElementById('reportTenantPreview');if(!v){el.textContent='Select a tenant bed.';return}const [pid,bed]=v.split('|');const t=(db.tenants||[]).find(x=>x.propertyId==pid&&String(x.bed)==bed);el.innerHTML=`${t?.name||'Tenant'}
Bed ${bed} Β· Contact: ${t?.contact||'β€”'} Β· Monthly rent: ${Number(t?.rent||0).toFixed(2)}`;} function tenantForPayment(x){return (db.tenants||[]).find(t=>t.propertyId==x.propertyId&&String(t.bed)==String(x.bed))} function generateTenantStatement(){ if(sessionRole!=='admin'&&sessionRole!=='viewer')return; const v=document.getElementById('reportBed').value;if(!v)return alert('Select a tenant.'); const [pid,bed]=v.split('|');const t=(db.tenants||[]).find(x=>x.propertyId==pid&&String(x.bed)==bed);const p=(db.properties||[]).find(x=>x.id==pid); const from=document.getElementById('reportFrom').value,to=document.getElementById('reportTo').value; const payments=(db.payments||[]).filter(x=>x.propertyId==pid&&String(x.bed)==bed&&(!from||x.date>=from)&&(!to||x.date<=to)).sort((a,b)=>(a.date||'').localeCompare(b.date||'')); const total=payments.reduce((s,x)=>s+Number(x.amount||0),0);const due=Number(t?.rent||0)*(from&&to?Math.max(1,Math.round((new Date(to)-new Date(from))/2592000000)+1):1);const balance=Math.max(0,due-total); document.getElementById('tenantReportOutput').innerHTML=`

AGBM β€” Tenant Payment Statement

${p?.name||''} Β· ${p?.unit||''} Β· Generated ${new Date().toLocaleDateString()}
Tenant
${t?.name||''}
Bed ${bed}
${t?.contact||''}
Period
${from||'All records'} to ${to||'Present'}
STATEMENT

${payments.map(x=>``).join('')||''}
DateAmountStatusComment
${x.date||''}${Number(x.amount||0).toFixed(2)}${x.status||'Paid'}${x.comment||''}
No payments recorded for this period.

Total paid${total.toFixed(2)}
Estimated due${due.toFixed(2)}
Balance${balance.toFixed(2)}

This statement is generated from AGBM records. Keep it for your payment proof.
`; } function previewReceipt(){ const id=document.getElementById('receiptPayment').value;const x=(db.payments||[]).find(q=>String(q.id)==String(id));const t=x&&tenantForPayment(x);const p=x&&(db.properties||[]).find(q=>q.id==x.propertyId);const el=document.getElementById('receiptPreview');if(!x||!t){el.innerHTML='';return} el.innerHTML=`

AGBM β€” RENT PAYMENT RECEIPT

${p?.name||''} Β· ${p?.unit||''}
Received from
${t.name}
Bed ${x.bed}
${t.contact||''}
Receipt No.
AGBM-${x.id}
Date
${x.date}

Payment received for rental accommodation.

Amount Paid: ${Number(x.amount||0).toFixed(2)}

Status:

Comment: ${x.comment||'β€”'}


AGBM β€” Aptek Global Business Manager
`; } function downloadReceipt(){ if(!document.getElementById('receiptPayment').value)return alert('Select a payment first.'); printReceipt(); } function printReceipt(){const v=document.getElementById('receiptPreview').innerHTML;if(!v)return alert('Select a payment first.');const w=window.open('','_blank');w.document.write(`AGBM Receipt${css} ${v}`);w.document.close();w.print();} function printTenantStatement(){const v=document.getElementById('tenantReportOutput').innerHTML;if(!v)return alert('Generate a statement first.');const w=window.open('','_blank');w.document.write(`AGBM Tenant Statement${css} ${v}`);w.document.close();w.print();} const _oldRender=render; render=function(){_oldRender();populateTenantReportSelectors();}; function copyTenantShareText(){ const v=document.getElementById('reportBed').value;if(!v)return alert('Select a tenant.'); const [pid,bed]=v.split('|');const t=(db.tenants||[]).find(x=>x.propertyId==pid&&String(x.bed)==bed);const text=`AGBM Payment Statement for ${t?.name||''} β€” Bed ${bed}. Please see the attached AGBM statement/receipt for payment records.`; navigator.clipboard?.writeText(text).then(()=>alert('Share message copied. You can paste it into WhatsApp, email or SMS.')); } const AGBM_DB_NAME='AGBM_LocalDB'; const AGBM_DB_VERSION=1; let agbmLocalDB=null; let agbmDBReady=false; let agbmLastSaved=null; function openAGBMDatabase(){ return new Promise((resolve,reject)=>{ if(!('indexedDB' in window)){reject(new Error('IndexedDB unavailable'));return;} const req=indexedDB.open(AGBM_DB_NAME,AGBM_DB_VERSION); req.onupgradeneeded=e=>{ const d=e.target.result; if(!d.objectStoreNames.contains('state')) d.createObjectStore('state',{keyPath:'id'}); if(!d.objectStoreNames.contains('backups')) d.createObjectStore('backups',{keyPath:'id'}); }; req.onsuccess=()=>{agbmLocalDB=req.result;resolve(req.result)}; req.onerror=()=>reject(req.error); }); } function idbGet(store,id){ return new Promise((resolve,reject)=>{ const tx=agbmLocalDB.transaction(store,'readonly'), s=tx.objectStore(store), r=s.get(id); r.onsuccess=()=>resolve(r.result); r.onerror=()=>reject(r.error); }); } function idbPut(store,value){ return new Promise((resolve,reject)=>{ const tx=agbmLocalDB.transaction(store,'readwrite'), s=tx.objectStore(store), r=s.put(value); r.onsuccess=()=>resolve(r.result); r.onerror=()=>reject(r.error); }); } async function loadLocalDatabase(){ try{ await openAGBMDatabase(); const row=await idbGet('state','main'); if(row && row.data){ db=row.data; db.properties=db.properties||[];db.payments=db.payments||[];db.expenses=db.expenses||[];db.tenantAccounts=db.tenantAccounts||[];db.requests=db.requests||[]; agbmRequests=db.requests; localStorage.setItem(KEY,JSON.stringify(db)); }else{ await idbPut('state',{id:'main',data:db,savedAt:new Date().toISOString()}); } agbmDBReady=true; agbmLastSaved=row?.savedAt||new Date().toISOString(); updateDBStatus(); if(typeof render==='function') render(); }catch(e){ agbmDBReady=false; const s=document.getElementById('dbStatus'); if(s)s.textContent='IndexedDB is unavailable in this browser; AGBM is using its compatibility storage.'; } } async async function saveLocalDatabase(){ if(sessionRole!=='admin')return alert('Administrator access required.'); if(!agbmDBReady)return alert('Local database is not ready yet.'); try{ const stamp=new Date().toISOString(); await idbPut('state',{id:'main',data:db,savedAt:stamp}); localStorage.setItem(KEY,JSON.stringify(db)); agbmLastSaved=stamp; updateDBStatus(); }catch(e){alert('Database save failed: '+e.message)} } function save(){ localStorage.setItem(KEY,JSON.stringify(db)); if(agbmDBReady && sessionRole==='admin'){ idbPut('state',{id:'main',data:db,savedAt:new Date().toISOString()}).then(()=>{ agbmLastSaved=new Date().toISOString(); updateDBStatus(); }).catch(()=>{}); } render(); } function createLocalBackup(){ if(sessionRole!=='admin')return alert('Administrator access required.'); const payload={format:'AGBM-LOCALDB',version:9,createdAt:new Date().toISOString(),data:db}; const blob=new Blob([JSON.stringify(payload,null,2)],{type:'application/json'}); const a=document.createElement('a');a.href=URL.createObjectURL(blob); a.download='AGBM_Backup_'+new Date().toISOString().slice(0,10)+'.json';a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1000); if(agbmDBReady) idbPut('backups',{id:'backup-'+Date.now(),payload,createdAt:payload.createdAt}).catch(()=>{}); } function restoreLocalBackup(event){ if(sessionRole!=='admin')return alert('Administrator access required.'); const file=event.target.files?.[0]; if(!file)return; const reader=new FileReader(); reader.onload=async()=>{ try{ const p=JSON.parse(reader.result); if(!p?.data?.properties || !p?.data?.payments || !p?.data?.expenses)throw new Error('Invalid AGBM backup'); if(!confirm('Restore this backup? Current local records will be replaced.'))return; db=p.data; await saveLocalDatabase(); render(); populateTenantReportSelectors?.(); alert('Backup restored successfully.'); }catch(e){alert('Restore failed: '+e.message)} event.target.value=''; }; reader.readAsText(file); } function verifyLocalDatabase(){ const problems=[]; if(!db||typeof db!=='object')problems.push('Database object missing'); ['properties','tenants','payments','expenses'].forEach(k=>{if(!Array.isArray(db[k]))problems.push(k+' collection missing')}); const props=new Set((db.properties||[]).map(p=>String(p.id))); (db.tenants||[]).forEach(t=>{if(!props.has(String(t.propertyId)))problems.push('Tenant '+t.name+' has no matching property')}); const el=document.getElementById('dbStatus'); if(el)el.textContent=problems.length?'Verification found '+problems.length+' issue(s): '+problems.slice(0,3).join('; '):'βœ“ Database verified. Core collections and property links are valid.'; } function updateDBStatus(){ const total=(db.properties||[]).length+(db.tenants||[]).length+(db.tenantAccounts||[]).length+(db.requests||[]).length+(db.payments||[]).length+(db.expenses||[]).length; const n=document.getElementById('dbRecords'),s=document.getElementById('dbSaved'); if(n)n.textContent=String(total); if(s)s.textContent=agbmLastSaved?new Date(agbmLastSaved).toLocaleString():'Never'; const st=document.getElementById('dbStatus'); if(st)st.textContent=agbmDBReady?'βœ“ IndexedDB local database is active. Your records can be used offline on this device.':'Local database is not available; compatibility storage is active.'; } const AGBM_REQUESTS_KEY='agbm_v10_requests'; let agbmRequests=db.requests||JSON.parse(localStorage.getItem(AGBM_REQUESTS_KEY)||'[]'); function syncRequests(){db.requests=agbmRequests;localStorage.setItem(AGBM_REQUESTS_KEY,JSON.stringify(agbmRequests));if(agbmDBReady&&sessionRole==='admin')idbPut('state',{id:'main',data:db,savedAt:new Date().toISOString()}).catch(()=>{});} function availableBedList(){const list=[];(db.properties||[]).forEach(p=>{(p.beds||[]).forEach(b=>{if(!b.tenant)list.push({property:p,bed:b.no})})});return list;} function renderTenantHome(){ const avail=availableBedList(),beds=document.getElementById('publicBeds'); const properties=(db.properties||[]); const flats=properties.length; const availableFlats=properties.filter(p=>availableBedList().some(x=>String(x.property.id)===String(p.id))).length; const summary=document.getElementById('publicAvailabilitySummary'); if(summary)summary.innerHTML=''+avail.length+' available bed(s) across '+availableFlats+' flat(s) β€’ '+flats+' published flat/property record(s).'; if(beds)beds.innerHTML=avail.length?'
'+avail.map(x=>`
${esc(x.property.name||'Property')}
${esc(x.property.unit||'Flat')} Β· Bed ${x.bed}
AVAILABLE
`).join('')+'
':'

No beds are currently marked available.

'; const info=document.getElementById('publicPropertyInfo'); if(info)info.innerHTML=(db.properties||[]).length?'':'

No properties have been published yet.

'; const sel=document.getElementById('reqProperty');if(sel){const current=sel.value;sel.innerHTML='';(db.properties||[]).forEach(p=>{if(availableBedList().some(x=>x.property.id==p.id))sel.innerHTML+=``});if([...sel.options].some(o=>o.value===current))sel.value=current;updateRequestBeds();} const wa=localStorage.getItem('agbm_public_whatsapp')||''; const wl=document.getElementById('publicWhatsAppLink'),wh=document.getElementById('publicWhatsAppHint'); if(wl){ if(wa){ const digits=wa.replace(/[^0-9]/g,''); wl.href='https://wa.me/'+digits; wl.style.display='inline-flex'; if(wh)wh.textContent='Tap the button to chat with HOPE Apartments management on WhatsApp.'; }else{ wl.removeAttribute('href'); wl.style.display='inline-flex'; wl.onclick=function(){alert('WhatsApp contact has not been published yet. Please check back later.');return false;}; if(wh)wh.textContent='WhatsApp contact will be available when management publishes its number.'; } } } function updateRequestBeds(){const sel=document.getElementById('reqProperty'),bed=document.getElementById('reqBed');if(!bed)return;const pid=sel?.value;bed.innerHTML='';availableBedList().filter(x=>!pid||String(x.property.id)===String(pid)).forEach(x=>bed.innerHTML+=``)} function submitBedRequest(){const name=(document.getElementById('reqName').value||'').trim(),contact=(document.getElementById('reqContact').value||'').trim(),identity=(document.getElementById('reqIdentity').value||'').trim();if(!name||!contact||!identity)return alert('Please enter your full name, phone/WhatsApp number, and Emirate ID/Passport Number.');const sel=document.getElementById('reqBed').value.split('|'),prop=document.getElementById('reqProperty').value;const r={id:'REQ-'+Date.now(),name,contact,identity,propertyId:sel[0]||prop||'',bed:sel[1]||'',neededDate:document.getElementById('reqDate').value,message:document.getElementById('reqMessage').value.trim(),createdAt:new Date().toISOString(),status:'New'};agbmRequests.push(r);syncRequests();const s=document.getElementById('requestStatus');s.style.display='block';s.textContent='βœ“ Request received. Management will contact you using the details provided.';document.getElementById('reqName').value='';document.getElementById('reqContact').value='';document.getElementById('reqIdentity').value='';document.getElementById('reqMessage').value='';} function openOldTenantLogin(){document.getElementById('tenantLoginModal').style.display='flex';document.getElementById('tenantLoginStatus').textContent='';document.getElementById('tenantLoginId').focus();} function closeTenantLogin(){document.getElementById('tenantLoginModal').style.display='none';} function tenantLogin(){const id=(document.getElementById('tenantLoginId').value||'').trim().toUpperCase();if(!id){document.getElementById('tenantLoginStatus').textContent='Please enter your Tenant Login ID.';return}const a=(db.tenantAccounts||[]).find(x=>String(x.loginId||'').toUpperCase()===id&&x.active!==false);if(!a){document.getElementById('tenantLoginStatus').textContent='Incorrect or inactive Tenant Login ID.';return}tenantSession={accountId:a.id};closeTenantLogin();document.getElementById('tenantHome').style.display='none';document.querySelector('.homeFooter').style.display='none';document.getElementById('tenantPortal').style.display='block';renderTenantPortal();} function tenantLogout(){tenantSession=null;document.getElementById('tenantPortal').style.display='none';document.querySelector('.homeFooter').style.display='flex';showTenantHome();} function getTenantAccount(){return tenantSession?(db.tenantAccounts||[]).find(x=>x.id===tenantSession.accountId):null} function getTenantRecord(a){if(!a)return null;let p=db.properties.find(x=>x.id===a.propertyId);let b=p?.beds?.find(x=>String(x.no)===String(a.bed));return {a,p,b}} function renderTenantPortal(){const a=getTenantAccount(),r=getTenantRecord(a),el=document.getElementById('tenantPortalContent');if(!a||!r?.p||!r?.b){tenantLogout();return}document.getElementById('tenantWelcome').textContent=`Welcome, ${r.b.tenant||a.name} β€’ ${r.p.name||''} / ${r.p.unit||''} β€’ Bed ${r.b.no}`;const pays=db.payments.filter(x=>x.property===a.propertyId&&String(x.bed)===String(a.bed)).slice().sort((x,y)=>String(y.date).localeCompare(String(x.date)));const paid=pays.reduce((z,x)=>z+(+x.amount||0),0),rent=+r.b.rent||+r.p.target||0;el.innerHTML=`

πŸ‘€ My Profile

Name: ${esc(r.b.tenant)}

Contact: ${esc(r.b.contact||a.contact||'β€”')}

Property: ${esc(r.p.name)} / ${esc(r.p.unit)}

Bed: ${r.b.no}

Move-in: ${esc(r.b.moveIn||'β€”')}

Monthly Rent: ${money(rent)}

TENANT ACCOUNT ACTIVE

πŸ’³ My Account Summary

${money(paid)}

Total recorded payments

Current monthly rent: ${money(rent)}

Payment records shown here belong only to your tenant account.

🧾 My Payment History

${pays.length?`${pays.map(x=>``).join('')}
DateAmountStatusComment
${esc(x.date)}${money(x.amount)}${esc(x.status)}${esc(x.comment||'')}
`:'

No payment records have been posted yet.

'}

πŸ“„ My Documents

Your management team can provide official statements and receipts from AGBM.

πŸ“ž Management

For rent, maintenance or account questions, contact management using the public contact details.

Phone: ${esc(localStorage.getItem('agbm_public_phone')||'Not set')}

WhatsApp: ${esc(localStorage.getItem('agbm_public_whatsapp')||localStorage.getItem('agbm_public_phone')||'Not set')}

`} function tenantPrintStatement(){const a=getTenantAccount(),r=getTenantRecord(a);if(!a||!r?.b)return;const pays=db.payments.filter(x=>x.property===a.propertyId&&String(x.bed)===String(a.bed)).slice().sort((x,y)=>String(x.date).localeCompare(String(y.date)));const w=window.open('','_blank');if(!w)return alert('Please allow pop-ups to print your statement.');w.document.write(`AGBM Tenant Statement

AGBM β€” Tenant Payment Statement

Tenant: ${esc(r.b.tenant)}
Property: ${esc(r.p.name)} / ${esc(r.p.unit)}
Bed: ${r.b.no}

${pays.map(x=>``).join('')}
DateAmountStatusComment
${esc(x.date)}${money(x.amount)}${esc(x.status)}${esc(x.comment||'')}
\x3Cscript>window.onload=()=>window.print()<\/script>`);w.document.close()} function openAdminBackdoor(){ if(sessionRole==='admin')return; document.getElementById('authGate').style.display='flex'; document.getElementById('authMessage').textContent=hasAdmin()?'Administrator login is required to access AGBM property records.':'Create your first Administrator PIN to protect AGBM.'; document.getElementById('loginPin').value=''; document.getElementById('loginPin').focus(); } function showTenantHome(){ tenantSession=null;sessionRole=null; document.getElementById('tenantPortal').style.display='none'; document.getElementById('adminShell').style.display='none'; document.getElementById('tenantHome').style.display='block'; document.querySelector('.homeFooter').style.display='flex'; renderTenantHome(); } function renderAdminRequests(){ const el=document.getElementById('adminRequests');if(!el)return; if(!agbmRequests.length){el.innerHTML='

No requests yet.

';return} el.innerHTML=''+agbmRequests.slice().reverse().map(r=>{ const p=(db.properties||[]).find(x=>String(x.id)===String(r.propertyId)); return ``; }).join('')+'
DateNameContact / IDProperty/BedNeededStatus
${new Date(r.createdAt).toLocaleString()}${esc(r.name)}${esc(r.contact)}
ID/Passport: ${esc(r.identity||'β€”')}
${esc(p?.name||'Any')} ${r.bed?'Β· Bed '+esc(r.bed):''}${esc(r.neededDate||'β€”')}${esc(r.status)}
'; } const _oldDBStatus=updateDBStatus; updateDBStatus=function(){_oldDBStatus();renderAdminRequests();}