+Number(current.price_usd||0).toFixed(2)+ ' / '+current.billing_interval : 'Custom'; $('billingMikrotikText').textContent= used+' of '+limit+' used'; $('billingDateLabel').textContent= managed?'Next Billing / Expiry':'Package Expiry'; $('billingDate').textContent=billingDateText( subscription?.current_period_end|| account.plan_expires_at ); $('billingStatus').textContent= subscription?.status||account.status||'active'; $('billingUsagePercent').textContent=percent+'%'; $('billingProgressBar').style.width=percent+'%'; $('billingPackages').innerHTML=packages.map(pkg=>{ const isCurrent= String(pkg.name).toLowerCase()=== String(account.plan_name||'').toLowerCase(); const button=isCurrent ? '' : managed ? '' : ''; return `
${isCurrent ? '
CURRENT
' : ''}

${billingEsc(pkg.name)}

${Number(pkg.price_usd||0).toFixed(2)} /${billingEsc(pkg.billing_interval)}

${billingEsc(pkg.description||'')}

${button}
`; }).join(''); }catch(error){ $('billingMessage').innerHTML= '
'+billingEsc(error.message)+'
'; } } async function requestBillingUpgrade(packageId,paypalManaged){ if(paypalManaged){ toast('PayPal approval flow will be enabled in Stage 2'); return; } if(!confirm('Send this package upgrade request to Admin?')){ return; } try{ const result=await api('/billing/upgrade-request',{ method:'POST', body:JSON.stringify({package_id:packageId}) }); toast(result.message); loadBilling(); }catch(error){ toast(error.message); } } function showPage(p){ document.querySelectorAll('.page').forEach(x=>x.classList.add('hidden')); $('page-'+p).classList.remove('hidden'); document.querySelectorAll('.nav button').forEach(x=>x.classList.toggle('active',x.dataset.page===p)); $('title').textContent={home:'Dashboard',vouchers:'Vouchers',create:'Create Voucher',download:'Voucher Download',resetmac:'Reset MAC',macbinding:'MAC Binding',pppoe:'PPPoE Customers','pppoe-expire':'PPPoE Expire',mikrotik:'MikroTik',usage:'Usage',billing:'Package & Billing',profile:'My Profile'}[p]; $('side').classList.remove('open'); if(p==='home')loadHome(); if(p==='vouchers')loadVouchers(); if(p==='macbinding'){ loadMacBindingPlans(); loadMacBindings(); } if(p==='pppoe')loadPppoe(); /* PPPOE EXPIRE PAGE LOAD V1 */ if(p==='pppoe-expire')loadPppoeExpire(); if(p==='mikrotik')loadMikrotiks(); syncMikrotikLiveRefresh(); if(p==='usage')loadUsage(); if(p==='download')loadDownloadPage(); if(p==='billing')loadBilling(); if(p==='profile')loadMe(); } document.querySelectorAll('[data-page]').forEach(b=>b.onclick=()=>showPage(b.dataset.page)); $('mobileMenu').onclick=()=>$('side').classList.toggle('open'); $('loginForm').onsubmit=async e=>{ e.preventDefault(); $('loginMsg').innerHTML=''; try{ const r=await fetch(API+'/auth/login',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ username:$('loginUser').value, password:$('loginPass').value }) }); const d=await r.json().catch(()=>({ success:false, message:'Invalid server response' })); if(!r.ok){ if(d.code==='RESELLER_PLAN_EXPIRED'){ $('loginMsg').innerHTML=`
${esc(d.message||'Your reseller plan has expired. Contact Admin.')}
WhatsApp: +968-77246677
Email: [email protected]
`; return; } throw new Error(d.message||'Login failed'); } localStorage.setItem('marufwifi_token',d.token); token=d.token; await start(); }catch(e){ $('loginMsg').innerHTML='
'+esc(e.message)+'
'; } }; $('logout').onclick=async()=>{try{await api('/auth/logout',{method:'POST'})}catch{}localStorage.removeItem('marufwifi_token');location.reload()}; async function start(){ // Keep both screens hidden while checking the saved session. $('login').classList.add('hidden'); $('app').classList.add('hidden'); if(!token){ $('login').classList.remove('hidden'); return; } try{ await loadMe(); // Valid token: show dashboard directly. $('login').classList.add('hidden'); $('app').classList.remove('hidden'); showPage('home'); }catch(e){ // Invalid/expired token: show login. $('app').classList.add('hidden'); $('login').classList.remove('hidden'); } } async function loadMe(){ const d=await api('/auth/me');me=d.reseller; const resellerDisplayName=me.name||me.username||'Reseller'; $('userBox').textContent=resellerDisplayName; $('resellerBrandName').textContent=resellerDisplayName; $('profileName').value=me.name||''; $('profileMobile').value=me.mobile||''; $('profileShop').value=me.shop_name||''; $('profileUser').value=me.username||''; } async function loadHome(){ const d=await api('/dashboard'); const v=d.stats.vouchers||{},p=d.stats.pppoe||{},m=d.stats.mikrotiks||{}; $('sTotal').textContent=v.total||0; $('sUnused').textContent=v.unused||0; $('sExpired').textContent=v.expired||0; $('sMikro').textContent=m.total||0; $('sPppoeTotal').textContent=p.total||0; $('sPppoeOnline').textContent=p.online||0; $('sHotspotOnline').textContent=v.online||0; // Reseller Information const planName = me?.plan_name || 'No Plan'; $('riPlan').textContent = planName; let durationText = 'โ€”'; let validityText = 'โ€”'; let statusText = 'Expired'; if(me?.plan_started_at && me?.plan_expires_at){ const start = new Date(me.plan_started_at); const expiry = new Date(me.plan_expires_at); const now = new Date(); const totalDays = Math.max( 0, Math.ceil((expiry - start) / 86400000) ); const remainingDays = Math.max( 0, Math.ceil((expiry - now) / 86400000) ); durationText = totalDays + ' Days'; if(expiry > now){ validityText = remainingDays + ' Days Left'; statusText = 'Active'; }else{ validityText = 'Expired'; statusText = 'Expired'; } } $('riDuration').textContent = durationText; $('riValidity').textContent = validityText; const usedMikro = Number(m.total || 0); const mikroLimit = Number(me?.mikrotik_limit || 0); $('riMikrotik').textContent = usedMikro + ' / ' + mikroLimit; $('riStatus').textContent = statusText; $('activityTable').innerHTML=(d.recent||[]).map(x=>`${esc(x.action)}${esc(x.details||'')}${fmt(x.created_at)}`).join('')||'No activity yet'; } async function loadVouchers(){const d=await api('/vouchers');vouchers=d.vouchers||[];renderVouchers()} function renderVouchers(){ const q=$('voucherSearch').value.toLowerCase(),s=$('voucherStatus').value; const a=vouchers.filter(v=>(!s||v.status===s)&&(!q||JSON.stringify(v).toLowerCase().includes(q))); $('voucherTable').innerHTML=a.map(v=>{ const id=Number(v.id); return ` ${esc(v.voucher_type==='code'?v.voucher_code:v.username)} ${esc(v.password||'-')} ${v.validity_days}d ${esc(v.speed_limit||'-')} ${esc(v.status)} ${esc(v.mac_address||'-')} ${fmt(v.expires_at)} ${v.status==='active' ?`` :v.status==='disabled' ?`` :'' } `; }).join('')||'No vouchers found'; } $('voucherSearch').oninput=renderVouchers; $('voucherStatus').onchange=renderVouchers; async function disableVoucher(id){ if(!confirm('Disable this voucher?'))return; try{await api('/vouchers/'+id+'/disable',{method:'POST'});toast('Voucher disabled');loadVouchers()}catch(e){toast(e.message)} } async function enableVoucher(id){ if(!confirm('Enable this voucher?'))return; try{ await api('/vouchers/'+id+'/enable',{method:'POST'}); toast('Voucher enabled'); await loadVouchers(); await loadHome(); }catch(e){ toast(e.message); } } async function deleteVoucher(id){ if(!confirm('Delete this voucher permanently?'))return; try{await api('/vouchers/'+id,{method:'DELETE'});toast('Voucher deleted');loadVouchers();loadHome()}catch(e){toast(e.message)} } function updateVoucherTypeUI(){ const type=$('vType').value; const manual=type==='manual'; $('manualUsernameField').style.display=manual?'':'none'; $('manualPasswordField').style.display=manual?'':'none'; const qty=$('vQty'); if(manual){ qty.value='1'; qty.min='1'; qty.max='1'; qty.disabled=true; }else{ qty.disabled=false; qty.min='1'; qty.max='300'; } $('vManualUsername').required=manual; $('vManualPassword').required=manual; } $('vType').addEventListener('change',updateVoucherTypeUI); updateVoucherTypeUI(); $('voucherForm').onsubmit=async e=>{ e.preventDefault();$('createMsg').innerHTML=''; const type=$('vType').value; if(type==='manual'){ $('vQty').value='1'; } try{ const d=await api('/vouchers/bulk',{method:'POST',body:JSON.stringify({ voucher_type:type, quantity:Number($('vQty').value), validity_days:Number($('vDays').value), download_speed:$('vDownloadSpeed').value, upload_speed:$('vUploadSpeed').value, username:type==='manual' ? $('vManualUsername').value.trim() : '', password:type==='manual' ? $('vManualPassword').value : '', comment:$('vComment').value })}); generated=d.vouchers||[]; $('generatedBox').classList.remove('hidden'); $('generated').innerHTML=generated.map(v=>`${esc(v.username||v.voucher_code)}${esc(v.password||'-')}${v.validity_days}d${esc(v.speed_limit)}${fmt(v.expires_at)}`).join(''); toast(d.count+' voucher(s) created'); loadHome(); }catch(e){$('createMsg').innerHTML='
'+esc(e.message)+'
'} }; $('downloadPdf').onclick=async()=>{ if(!generated.length){toast('No generated vouchers to download');return} try{ if(!window.jspdf?.jsPDF)throw new Error('PDF library did not load. Check VPS internet/browser connection.'); const profile=await api('/reseller/pdf-profile'); const {jsPDF}=window.jspdf; const doc=new jsPDF({unit:'mm',format:'a4'}); /* * VOUCHER CODE ONLY * A4 = 40 vouchers * 5 columns x 8 rows */ const marginX=5; const marginY=5; const cardW=37.5; const cardH=34; const gapX=1.5; const gapY=1.5; const columns=5; const rows=8; const perPage=40; const resellerName= profile.reseller.shop_name || profile.reseller.name || profile.reseller.username || 'Reseller'; for(let i=0;i0 && i%perPage===0){ doc.addPage(); } const pos=i%perPage; const col=pos%columns; const row=Math.floor(pos/columns); const x=marginX+col*(cardW+gapX); const y=marginY+row*(cardH+gapY); /* Card */ doc.setDrawColor(150); doc.setLineWidth(0.3); doc.roundedRect( x, y, cardW, cardH, 1.5, 1.5, 'S' ); /* Small serial number */ doc.setFont('helvetica','normal'); doc.setFontSize(4); doc.text( String(i+1), x+cardW-2, y+4, {align:'right'} ); /* Reseller name */ doc.setFont('helvetica','bold'); doc.setFontSize(7); doc.text( String(resellerName).slice(0,22), x+cardW/2, y+8, {align:'center'} ); /* Voucher label */ doc.setFont('helvetica','normal'); doc.setFontSize(4); doc.text( 'VOUCHER CODE', x+cardW/2, y+13, {align:'center'} ); /* BIG voucher code */ const v=generated[i]; const code=String( v.voucher_code || v.username || v.code || '-' ); let codeSize=15; if(code.length>10) codeSize=13; if(code.length>12) codeSize=11; if(code.length>14) codeSize=9.5; doc.setFont('helvetica','bold'); doc.setFontSize(codeSize); doc.text( code.slice(0,18), x+cardW/2, y+21, {align:'center'} ); /* Divider */ doc.setLineWidth(0.2); doc.line( x+4, y+23, x+cardW-4, y+23 ); /* Validity */ const validity=Number( v.validity_days || v.validity || 0 ); doc.setFont('helvetica','normal'); doc.setFontSize(6); doc.text( 'Validity: '+(validity || '-')+' Day'+ (validity===1?'':'s'), x+cardW/2, y+28, {align:'center'} ); /* Bottom serial */ doc.setFontSize(3.5); doc.text( '#'+String(i+1), x+cardW/2, y+32, {align:'center'} ); } const safe=(profile.reseller.shop_name||profile.reseller.name||'reseller').replace(/[^a-z0-9_-]+/gi,'-'); doc.save('marufwifi-vouchers-'+safe+'.pdf'); }catch(e){toast(e.message)} }; async function loadDownloadPage(){ try{ const d=await api('/vouchers'); vouchers=d.vouchers||[]; updateDownloadValidityOptions(); updateDownloadCount(); }catch(e){ $('downloadCount').textContent=e.message; } } function updateDownloadValidityOptions(){ const select=$('downloadValidity'); if(!select)return; const current=select.value; const days=[...new Set( vouchers .filter(v=>String(v.status||'').toLowerCase()==='active') .map(v=>Number(v.validity_days)) .filter(n=>Number.isInteger(n) && n>0) )].sort((a,b)=>a-b); select.innerHTML= ''+ days.map(d=> '' ).join(''); if(days.includes(Number(current))){ select.value=current; } } function getDownloadVouchers(){ const validity=$('downloadValidity').value; const mode=$('downloadUnused').value; return vouchers.filter(v=>{ if(String(v.status||'').toLowerCase()!=='active'){ return false; } if(mode==='unused' && v.mac_address){ return false; } if( validity && Number(v.validity_days)!==Number(validity) ){ return false; } return true; }); } function updateDownloadCount(){ const list=getDownloadVouchers(); $('downloadCount').textContent= list.length+ ' voucher(s) match the selected filters.'; } $('downloadValidity').onchange=updateDownloadCount; $('downloadUnused').onchange=updateDownloadCount; $('downloadVoucherPdf').onclick=async()=>{ const list=getDownloadVouchers(); if(!list.length){ toast('No voucher found for the selected filters'); return; } try{ if(!window.jspdf?.jsPDF){ throw new Error( 'PDF library did not load. Please refresh the page.' ); } const profile=await api('/reseller/pdf-profile'); const {jsPDF}=window.jspdf; const doc=new jsPDF({ unit:'mm', format:'a4', orientation:'portrait' }); /* * ========================================== * MARUF WIFI - FINAL VOUCHER DESIGN * 40 VOUCHERS / A4 * 4 COLUMNS x 10 ROWS * ========================================== */ const pageW=210; const pageH=297; const marginX=5; const marginY=5; const cardW=48.5; const cardH=27.5; const gapX=2.0; const gapY=1.2; const columns=4; const rows=10; const perPage=40; const resellerName= profile.reseller.shop_name || profile.reseller.name || profile.reseller.username || 'Reseller'; for(let i=0;i0 && i%perPage===0){ doc.addPage(); } const pos=i%perPage; const col=pos%columns; const row=Math.floor(pos/columns); const x= marginX+ col*(cardW+gapX); const y= marginY+ row*(cardH+gapY); /* * ================================== * CARD BORDER * ================================== */ doc.setDrawColor(80,80,80); doc.setLineWidth(0.35); doc.roundedRect( x, y, cardW, cardH, 2, 2, 'S' ); /* * Small black number badge */ doc.setFillColor(25,25,25); doc.roundedRect( x, y, 8, 7, 1.5, 1.5, 'F' ); doc.setTextColor(255,255,255); doc.setFont('helvetica','bold'); doc.setFontSize(5.5); doc.text( String(i+1).padStart(2,'0'), x+4, y+4.8, {align:'center'} ); /* * Reset text color */ doc.setTextColor(20,20,20); /* * ================================== * WIFI SYMBOL * ================================== */ doc.setLineWidth(0.7); doc.setDrawColor(20,20,20); /* * Simple WiFi icon made with arcs/lines */ doc.line( x+11.5, y+5.0, x+13.5, y+3.5 ); doc.line( x+13.5, y+3.5, x+15.5, y+5.0 ); doc.line( x+12.3, y+6.5, x+13.5, y+5.4 ); doc.line( x+13.5, y+5.4, x+14.7, y+6.5 ); doc.circle( x+13.5, y+8.0, 0.8, 'F' ); /* * ================================== * RESELLER NAME * ================================== */ doc.setFont('helvetica','bold'); doc.setFontSize(7.5); doc.setTextColor(15,15,15); doc.text( String(resellerName).slice(0,22), x+17, y+5.2 ); /* * Subtitle */ doc.setFont('helvetica','normal'); doc.setFontSize(4.2); doc.text( 'WiFi Hotspot Voucher', x+17, y+8.2 ); /* * ================================== * VALIDITY BADGE * ================================== */ const validity= (v.validity_days||'-')+ ' Day'+ ((Number(v.validity_days)||0)===1?'':'s'); doc.setFillColor(25,25,25); doc.roundedRect( x+cardW-13, y+2, 10.5, 8.5, 1.5, 1.5, 'F' ); doc.setTextColor(255,255,255); doc.setFont('helvetica','bold'); doc.setFontSize(5.5); doc.text( String(v.validity_days||'-')+'D', x+cardW-7.75, y+5.8, {align:'center'} ); doc.setFontSize(3.2); doc.text( 'VALIDITY', x+cardW-7.75, y+8.2, {align:'center'} ); /* * ================================== * USERNAME LABEL * ================================== */ doc.setTextColor(25,25,25); doc.setFont('helvetica','bold'); doc.setFontSize(3.5); doc.text( 'USERNAME', x+2.5, y+13 ); /* * Username box */ const login= v.voucher_type==='code' ?(v.voucher_code||'-') :(v.username||'-'); doc.setDrawColor(150,150,150); doc.setLineWidth(0.25); doc.roundedRect( x+2.2, y+13.7, cardW-4.4, 5.0, 1, 1, 'S' ); /* * Username value */ doc.setTextColor(10,10,10); doc.setFont('helvetica','bold'); doc.setFontSize(8.5); doc.text( String(login).slice(0,20), x+cardW/2, y+17.3, {align:'center'} ); /* CODE ONLY PDF HIDE PASSWORD V1 */ if(v.voucher_type!=='code'){ /* * ================================== * PASSWORD LABEL * ================================== */ doc.setFont('helvetica','bold'); doc.setFontSize(3.5); doc.text( 'PASSWORD', x+2.5, y+20.8 ); /* * Password box */ doc.roundedRect( x+2.2, y+21.5, cardW-4.4, 4.8, 1, 1, 'S' ); /* * Password value */ doc.setFont('helvetica','bold'); doc.setFontSize(8.2); doc.text( String(v.password||'-').slice(0,20), x+cardW/2, y+25, {align:'center'} ); } /* * ================================== * SMALL CORNER DESIGN * ================================== */ doc.setFillColor(25,25,25); doc.triangle( x+cardW-6, y, x+cardW, y, x+cardW, y+6, 'F' ); /* * Reset for next voucher */ doc.setTextColor(20,20,20); } /* * ================================== * FILE NAME * ================================== */ const safe=( profile.reseller.shop_name || profile.reseller.name || profile.reseller.username || 'reseller' ).replace( /[^a-z0-9_-]+/gi, '-' ); doc.save( 'marufwifi-vouchers-'+ safe+ '.pdf' ); toast( list.length+ ' voucher(s) downloaded as PDF' ); }catch(e){ toast(e.message); } };;; $('resetSearchBtn').onclick=searchResetMac; $('resetSearch').onkeydown=e=>{if(e.key==='Enter')searchResetMac()}; async function searchResetMac(){ const q=$('resetSearch').value.trim(); $('resetMsg').innerHTML='';$('resetResults').innerHTML=''; if(q.length<2){$('resetMsg').innerHTML='
Enter at least 2 characters.
';return} try{ const d=await api('/reseller/reset-mac/search?q='+encodeURIComponent(q)); if(!d.vouchers.length){$('resetResults').innerHTML='
No matching voucher found.
';return} $('resetResults').innerHTML=d.vouchers.map(v=>`
Voucher: ${esc(v.voucher_type==='code'?v.voucher_code:v.username)}
Password: ${esc(v.password||'-')}
Speed: ${esc(v.speed_limit||'-')}
Status: ${esc(v.status)}
Expiry: ${fmt(v.expires_at)}
Current MAC: ${esc(v.mac_address||'Not assigned')}
${v.mac_address?``:'No MAC is currently assigned.'}
`).join(''); }catch(e){$('resetMsg').innerHTML='
'+esc(e.message)+'
'} } async function doResetMac(id){ if(!confirm('Reset this voucher MAC address?'))return; try{await api('/reseller/reset-mac',{method:'POST',body:JSON.stringify({id})});toast('MAC reset successfully');searchResetMac()}catch(e){toast(e.message)} } async function loadPppoe(){ try{ const [d,m]=await Promise.all([api('/pppoe'),api('/mikrotiks')]); pppoe=Array.isArray(d.customers)?d.customers:[]; const eligible=(m.mikrotiks||[]).filter(x=>x.approval_status==='approved'&&x.status==='active'); const current=$('pMikro').value; $('pMikro').innerHTML=''+eligible.map(x=> `` ).join(''); if(eligible.some(x=>String(x.id)===String(current)))$('pMikro').value=current; renderPppoe(); }catch(e){ $('ppTable').innerHTML='Unable to load PPPoE customers'; toast(e.message); } } function renderPppoe(){ const q=String($('ppSearch').value||'').toLowerCase().trim(); const rows=pppoe.filter(x=>!q||String(x.customer_name||'').toLowerCase().includes(q)||String(x.username||'').toLowerCase().includes(q)); $('ppTable').innerHTML=rows.map(x=>` ${esc(x.customer_name||'-')} ${esc(x.username||'-')} ${esc(x.mikrotik_name||'-')} ${esc(x.download||x.download_speed||'-')} ${esc(x.upload||x.upload_speed||'-')} ${esc(x.status||'-')} ${fmt(x.expires_at)} ${x.status==='expired'?'':``} `).join('')||'No PPPoE customers found'; } if($('ppSearch'))$('ppSearch').oninput=renderPppoe; if($('refreshPppoe'))$('refreshPppoe').onclick=loadPppoe; if($('pppoeForm')){ $('pppoeForm').onsubmit=async e=>{ e.preventDefault(); $('pppoeMsg').innerHTML=''; try{ await api('/pppoe',{method:'POST',body:JSON.stringify({ customer_name:$('pName').value.trim(),username:$('pUser').value.trim(), password:$('pPass').value,download_speed:$('pDownload').value.trim(), upload_speed:$('pUpload').value.trim(),validity_days:Number($('pDays').value), mikrotik_id:Number($('pMikro').value),comment:$('pComment').value })}); toast('PPPoE customer created'); e.target.reset(); $('pDays').value=30;$('pDownload').value='20M';$('pUpload').value='10M'; await loadPppoe();await loadHome(); }catch(err){ $('pppoeMsg').innerHTML='
'+esc(err.message)+'
'; } }; } async function toggleP(id){ if(!confirm('Change this PPPoE customer status?'))return; try{ const d=await api('/pppoe/'+id+'/toggle',{method:'POST'}); toast(d.status==='disabled'?'Customer disabled and disconnected':'Customer enabled'); await loadPppoe();await loadPppoeExpire();await loadHome(); }catch(e){toast(e.message)} } async function editPppoe(id){ const c=pppoe.find(x=>Number(x.id)===Number(id)); if(!c)return; const customerName=prompt('Customer Name:',c.customer_name||'');if(customerName===null)return; const password=prompt('Password:',c.password||'');if(password===null)return; const download=prompt('Download Speed:',c.download||c.download_speed||'20M');if(download===null)return; const upload=prompt('Upload Speed:',c.upload||c.upload_speed||'10M');if(upload===null)return; const days=prompt('Validity Days:',c.validity_days||30);if(days===null)return; const router=prompt('MikroTik ID:',c.mikrotik_id||'');if(router===null)return; const comment=prompt('Comment:',c.comment||'');if(comment===null)return; try{ await api('/pppoe/'+id,{method:'PUT',body:JSON.stringify({customer_name:customerName, password,download_speed:download,upload_speed:upload,validity_days:Number(days), mikrotik_id:Number(router),comment})}); toast('PPPoE customer updated');await loadPppoe();await loadPppoeExpire(); }catch(e){alert(e.message)} } async function deletePppoe(id){ const c=pppoe.find(x=>Number(x.id)===Number(id)); if(!confirm('Delete '+(c?.customer_name||'this PPPoE customer')+' permanently?'))return; try{ await api('/pppoe/'+id,{method:'DELETE'}); toast('PPPoE customer deleted and disconnected'); await loadPppoe();await loadPppoeExpire();await loadHome(); }catch(e){alert(e.message)} } function formatBytesPPPoE(bytes){ bytes=Number(bytes||0); if(bytes>=1024*1024*1024) return (bytes/(1024*1024*1024)).toFixed(2)+' GB'; if(bytes>=1024*1024) return (bytes/(1024*1024)).toFixed(2)+' MB'; if(bytes>=1024) return (bytes/1024).toFixed(2)+' KB'; return bytes+' B'; } async function loadPppoeExpire(){ try{ const d=await api('/pppoe-expire'); pppoeExpire=Array.isArray(d.customers) ? d.customers : []; renderPppoeExpire(); }catch(e){ console.error('PPPoE Expire:',e); pppoeExpire=[]; $('ppExpireTable').innerHTML= 'Unable to load customers'; } } function searchPppoeExpire(){ renderPppoeExpire(); } if($('searchPppExpire')){ $('searchPppExpire').onclick=searchPppoeExpire; } if($('ppExpireSearch')){ $('ppExpireSearch').onkeydown=e=>{ if(e.key==='Enter'){ e.preventDefault(); searchPppoeExpire(); } }; } function renderPppoeExpire(){ const q=($('ppExpireSearch').value||'').toLowerCase().trim(); const rows=pppoeExpire.filter(x=>!q||String(x.customer_name||'').toLowerCase().includes(q)||String(x.username||'').toLowerCase().includes(q)); $('ppExpireTable').innerHTML=rows.map(x=>{ const download=x.download||x.download_speed||'-'; const upload=x.upload||x.upload_speed||'-'; const monthlyDownload=Number(x.download_bytes||0); const monthlyUpload=Number(x.upload_bytes||0); const online=x.online_status==='online'; const status=''+esc(x.status)+''; const toggle=x.status==='expired'?'':``; return ` ${esc(x.customer_name||'-')}${esc(x.username||'-')} ${online?'๐ŸŸข Online':'๐Ÿ”ด Offline'} ${esc(download)}${esc(upload)} ${formatBytesPPPoE(monthlyDownload)}${formatBytesPPPoE(monthlyUpload)} ${formatBytesPPPoE(monthlyDownload+monthlyUpload)} ${fmt(x.expires_at)}${status}
${toggle}
`; }).join('')||'No PPPoE customers found'; } async function togglePppoeExpire(id){ if(!confirm('Are you sure you want to change this PPPoE status?')) return; try{ await api('/pppoe-expire/'+id+'/toggle',{ method:'POST' }); toast('PPPoE status updated'); await loadPppoeExpire(); }catch(e){ toast(e.message); } } async function payPppoe(id){ const customer=pppoeExpire.find( x=>Number(x.id)===Number(id) ); if(!customer) return; const amount=prompt( 'Enter Paid Amount:', customer.last_paid_amount || '' ); if(amount===null) return; const value=Number(amount); if(!Number.isFinite(value) || value<0){ alert('Please enter a valid amount'); return; } const message= 'Renew '+ (customer.customer_name||customer.username)+ ' for '+ (customer.validity_days||30)+ ' days?'; if(!confirm(message)) return; try{ const d=await api( '/pppoe-expire/'+id+'/pay', { method:'POST', body:JSON.stringify({ paid_amount:value }) } ); toast( 'Renewed successfully. New expiry: '+ fmt(d.new_expires_at) ); await loadPppoeExpire(); }catch(e){ alert(e.message); } } if($('ppExpireSearch')) $('ppExpireSearch').oninput=renderPppoeExpire; function updateMikrotikMethodUI(){ const method = $('mMethod').value; const os = $('mOS'); const keyField = $('mKeyField'); const key = $('mKey'); if(method === 'wireguard'){ os.innerHTML = ''; os.value = '7'; keyField.style.display = ''; key.required = true; }else{ os.innerHTML = '' + ''; if(os.value !== '7' && os.value !== '6'){ os.value = '7'; } keyField.style.display = 'none'; key.required = false; key.value = ''; } } $('mMethod').addEventListener('change', updateMikrotikMethodUI); updateMikrotikMethodUI(); async function loadMikrotiks(){ const d=await api('/mikrotiks'); mikrotiks=d.mikrotiks||[]; const l=await api('/mikrotiks/limit'); $('mLimit').textContent=`Used ${l.used} / ${l.limit} ยท Deleted devices do not return a slot`; $('mTable').innerHTML=mikrotiks.map(x=>{ const lastSeen=x.last_seen?new Date(x.last_seen).getTime():0; let liveStatus='Offline'; let statusClass='disabled'; if(x.approval_status==='pending'){ liveStatus='Pending'; statusClass='pending'; }else if(x.approval_status==='rejected'){ liveStatus='Rejected'; statusClass='rejected'; }else if(x.status!=='active'){ liveStatus='Disabled'; statusClass='disabled'; }else if(x.online===true||(lastSeen&&Date.now()-lastSeen<=5*60*1000)){ liveStatus='Online'; statusClass='active'; } let actions=''; if(x.approval_status==='approved'&&!x.setup_complete){ actions+=` `; }else if(x.approval_status!=='approved'){ actions+='Waiting for approval '; } actions+=``; return ` ${esc(x.name||'-')} ${esc(String(x.connection_method||'-').toUpperCase())} ${esc(x.router_os?'RouterOS '+x.router_os:'-')} ${esc(x.vpn_ip||x.wg_address||'-')} ${esc(x.approval_status||'pending')} ${liveStatus} ${actions} `; }).join('')||'No MikroTik'; } /* MIKROTIK LIVE STATUS V1 */ let mikrotikLiveRefreshTimer=null; let mikrotikLiveRefreshBusy=false; const MIKROTIK_LIVE_REFRESH_MS=3000; function mikrotikPageIsVisible(){ const page=$('page-mikrotik'); return Boolean( token && page && !page.classList.contains('hidden') && !document.hidden ); } async function refreshMikrotikLiveStatus(){ if(!mikrotikPageIsVisible() || mikrotikLiveRefreshBusy)return; mikrotikLiveRefreshBusy=true; try{ await loadMikrotiks(); }catch(e){ // A temporary network error must not stop future live refreshes. console.warn('MikroTik live-status refresh:',e.message); }finally{ mikrotikLiveRefreshBusy=false; } } function syncMikrotikLiveRefresh(){ if(mikrotikLiveRefreshTimer){ clearInterval(mikrotikLiveRefreshTimer); mikrotikLiveRefreshTimer=null; } if(mikrotikPageIsVisible()){ mikrotikLiveRefreshTimer=setInterval( refreshMikrotikLiveStatus, MIKROTIK_LIVE_REFRESH_MS ); } } document.addEventListener('visibilitychange',()=>{ syncMikrotikLiveRefresh(); if(!document.hidden)refreshMikrotikLiveStatus(); }); window.addEventListener('pagehide',()=>{ if(mikrotikLiveRefreshTimer){ clearInterval(mikrotikLiveRefreshTimer); mikrotikLiveRefreshTimer=null; } }); async function deleteMikrotik(id){ const item=mikrotiks.find(x=>Number(x.id)===Number(id)); const name=item?.name||('MikroTik #'+id); if(!confirm('Delete '+name+'? This will not return the used MikroTik limit.'))return; try{ const d=await api('/mikrotiks/'+id,{method:'DELETE'}); toast(d.message||'MikroTik deleted'); await loadMikrotiks(); }catch(e){ alert(e.message); } } if($('mForm')){ $('mForm').onsubmit=async e=>{ e.preventDefault(); $('mMsg').innerHTML=''; try{ let d=await api('/mikrotiks',{ method:'POST', body:JSON.stringify({ name:$('mName').value, host:$('mHost').value, username:$('mUser').value, api_port:Number($('mPort').value), connection_method:$('mMethod').value, router_os:$('mOS').value, wg_public_key:$('mKey').value }) }); toast('MikroTik added: '+d.mikrotik.approval_status); e.target.reset(); $('mPort').value=8728; loadMikrotiks(); }catch(e){ $('mMsg').innerHTML='
'+esc(e.message)+'
'; } }; } async function getScript(id){try{let d=await api('/mikrotiks/'+id+'/script');let w=window.open('','_blank');w.document.write('
'+esc(d.script)+'
')}catch(e){toast(e.message)}} /* ============================================================ MAC BINDING UI - AUTOLOGIN V4 ============================================================ */ function mbNormalizeMac(value){ let x=String(value||'').toUpperCase().replace(/[^0-9A-F]/g,'').slice(0,12); return x.match(/.{1,2}/g)?.join(':')||''; } function mbDisplayStatus(x){ return String(x.display_status||x.status||'').toLowerCase(); } function mbStatusBadge(x){ const status=mbDisplayStatus(x); return `${esc(status||'unknown')}`; } function mbSpeedParts(x){ const pair=String(x.speed_limit||'').split('/'); return { download:String(x.download_speed||pair[0]||'10M'), upload:String(x.upload_speed||pair[1]||pair[0]||'10M') }; } async function loadMacBindingPlans(){return;} function renderMacBindings(){ const q=String($('mbSearch')?.value||'').toLowerCase().trim(); const rows=macBindings.filter(x=> !q||JSON.stringify(x).toLowerCase().includes(q) ); $('macBindingTable').innerHTML=rows.map(x=>{ const speed=mbSpeedParts(x); const id=Number(x.id); return ` ${esc(x.customer_name||'Unnamed Device')} ${esc(x.mac_address)} ${esc(x.validity_days)} Days ${esc(speed.download)} ${esc(speed.upload)} ${fmt(x.started_at)} ${fmt(x.expires_at)} ${mbStatusBadge(x)} `; }).join('')||'No MAC bindings found'; } async function loadMacBindings(){ try{ const data=await api('/mac-binding'); macBindings=data.bindings||[]; renderMacBindings(); }catch(e){ $('macBindingTable').innerHTML=`
${esc(e.message)}
`; } } $('mbMac').addEventListener('input',function(){ this.value=mbNormalizeMac(this.value); }); $('mbSearch').addEventListener('input',renderMacBindings); $('mbRefresh').addEventListener('click',async function(){ await loadMacBindings(); toast('MAC Binding refreshed'); }); $('macBindingForm').addEventListener('submit',async function(e){ e.preventDefault(); const msg=$('macBindingMsg'); msg.innerHTML=''; const customerName=$('mbName').value.trim(); const mac=mbNormalizeMac($('mbMac').value); const days=Number($('mbDays').value); const download=$('mbDownload').value.trim(); const upload=$('mbUpload').value.trim(); if(!customerName){ msg.innerHTML='
Please enter a customer or device name.
'; return; } if(!/^([0-9A-F]{2}:){5}[0-9A-F]{2}$/.test(mac)){ msg.innerHTML='
Please enter a valid MAC address.
'; return; } if(!Number.isInteger(days)||days<1||days>3650){ msg.innerHTML='
Validity must be between 1 and 3650 days.
'; return; } const btn=$('mbCreateBtn'); btn.disabled=true; btn.textContent='Binding...'; try{ const data=await api('/mac-binding',{ method:'POST', body:JSON.stringify({ customer_name:customerName, mac_address:mac, validity_days:days, download_speed:download, upload_speed:upload }) }); if(!data.success)throw new Error(data.message||'MAC Binding failed'); $('mbName').value=''; $('mbMac').value=''; toast( data.router_setup?.failed ? 'Binding created, but MikroTik MAC login setup was not confirmed' : 'MAC Binding created and MAC login enabled' ); await loadMacBindings(); }catch(e){ msg.innerHTML='
'+esc(e.message)+'
'; }finally{ btn.disabled=false; btn.textContent='๐Ÿ”— Bind MAC'; } }); async function deleteMacBinding(id){ if(!confirm('Delete this MAC Binding? Internet will stop immediately.'))return; try{ const data=await api('/mac-binding/'+id,{method:'DELETE'}); if(!data.success)throw new Error(data.message||'Delete failed'); toast(data.message||'MAC Binding deleted'); await loadMacBindings(); }catch(e){toast(e.message)} } /* ============================================================ END MAC BINDING UI ============================================================ */ async function loadUsage(){let d=await api('/usage');$('usageTable').innerHTML=(d.usage||[]).map(x=>`${esc(x.username||x.voucher_code||'-')}${esc(x.comment||'')}${bytes(x.input_bytes)}${bytes(x.output_bytes)}${fmt(x.last_start)}`).join('')||'No usage data'} /* ========================================================= FINAL MY PROFILE EDIT / SAVE ========================================================= */ ; function fmt(v){return v?new Date(v).toLocaleString():'-'} function bytes(n){n=Number(n||0);if(!n)return'0 B';let u=['B','KB','MB','GB','TB'],i=Math.floor(Math.log(n)/Math.log(1024));return(n/Math.pow(1024,i)).toFixed(i?2:0)+' '+u[i]} function esc(v){return String(v??'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]))} start(); /* ========================================================= MY PROFILE EDIT / SAVE ========================================================= */ ; /* ========================================================= CLEAN MY PROFILE CONTROLLER ========================================================= */ ;