🏫
Sistem Yuran PIBG
Semakan & Pembayaran
Admin
Portal Ibu Bapa & Penjaga

Semak & Selesaikan
Yuran PIBG Anda

Semak status pembayaran anak anda dengan mudah dan cepat

Borang Pembayaran Yuran
1
Maklumat
2
Pembayaran
Selesai

Sila buat bayaran ke akaun berikut sebelum menghantar resit:

Nama Bank
—
—
Jumlah Bayaran RM 0.00
Saya telah membuat bayaran sebanyak RM 0.00 ke akaun di atas.
Tick untuk aktifkan butang hantar resit kepada bendahari.
Mesej akan dibuka di WhatsApp anda
Berjaya!
Maklumat bayaran telah dihantar kepada bendahari PIBG melalui WhatsApp.

Resit rasmi akan dihantar oleh bendahari selepas bayaran disahkan.

Panel Admin
Dashboard
Notifikasi
Keluarga & Anak
Bayaran
Import / Export
Tetapan
Notifikasi Bayaran
Dashboard
Pembayaran Terkini
KeluargaBil. AnakYuranStatusTarikh
Keluarga & Anak

Yuran dikenakan sekali per keluarga tidak kira berapa ramai anak. Klik + Anak untuk tambah anak dalam sesebuah keluarga.

Pengurusan Bayaran
KeluargaNo. TelJumlahStatusSudah BayarTindakan
Import & Export
Upload Data (CSV)

Klik atau seret fail CSV di sini

Format: NamaKeluarga, Telefon, Yuran, NamaAnak1|Kelas1, NamaAnak2|Kelas2…

Export Data
Tetapan Sistem
Nombor Bendahari PIBG

Nombor ini disambungkan ke WhatsApp untuk penerimaan bayaran daripada pengguna.

Maklumat Akaun Bank PIBG
Maklumat Sekolah
🔐
Log Masuk Admin
Kawasan terhad untuk pentadbir sahaja
Kata laluan salah.
Tambah Keluarga
Tambah Anak
Jana Resit
// ───────────────────────────────────────────────────────── let adminToken = null; let allStudents = []; let settings = {}; let curFam = null, curRecFam = null, editId = null, payData = {}, debTimer = null; // ─── HELPERS ────────────────────────────────────────────── async function api(path, method='GET', body=null) { const opts = { method, headers: {'Content-Type':'application/json'} }; if (adminToken) opts.headers['Authorization'] = `Bearer ${adminToken}`; if (body) opts.body = JSON.stringify(body); const res = await fetch(API_URL + path, opts); if (!res.ok) { const e = await res.json().catch(()=>({})); throw new Error(e.error||'Ralat API'); } return res.json(); } function groupByFamily(rows) { const m = {}; rows.forEach(r => { if (!m[r.family_name]) m[r.family_name]=[]; m[r.family_name].push(r); }); return m; } // ─── INIT ───────────────────────────────────────────────── async function init() { try { settings = await api('/api/settings/public'); applyPublicSettings(); } catch(e) { console.warn('API tidak dapat disambung. Gunakan data lalai.'); settings = { bank_name:'Bank Islam Malaysia', bank_number:'1234 5678 9012', bank_holder:'PIBG SMK Contoh Jaya', school_name:'SMK Contoh Jaya', school_year:'2025', treasurer_phone:'601123456789', default_fee:'50' }; applyPublicSettings(); } } function applyPublicSettings() { const sn = settings.school_name || 'Sistem Yuran PIBG'; const yr = settings.school_year || ''; document.getElementById('navSchoolName').textContent = sn; document.getElementById('navYear').textContent = yr ? 'Yuran PIBG ' + yr : 'Semakan & Pembayaran'; if (settings.bank_name) document.getElementById('bkLbl').textContent = settings.bank_name; if (settings.bank_number) document.getElementById('bkNum').textContent = settings.bank_number; if (settings.bank_holder) document.getElementById('bkHolder').textContent = settings.bank_holder; } // ─── NAVIGATION ─────────────────────────────────────────── function showScreen(n) { document.querySelectorAll('.screen').forEach(s => s.classList.remove('active')); document.getElementById('screen' + n.charAt(0).toUpperCase() + n.slice(1)).classList.add('active'); } function switchPanel(p) { document.getElementById('tabUser').classList.toggle('active', p==='user'); document.getElementById('tabAdmin').classList.toggle('active', p==='admin'); showScreen(p); if (p==='admin') { aSection('dashboard'); } } // ─── LOGIN ──────────────────────────────────────────────── function openLoginModal() { document.getElementById('loginModal').classList.add('open'); document.getElementById('loginErr').style.display = 'none'; setTimeout(()=>document.getElementById('lu').focus(), 100); } function closeLoginModal() { document.getElementById('loginModal').classList.remove('open'); } async function doLogin() { const u = document.getElementById('lu').value; const p = document.getElementById('lp').value; if (u !== 'admin') { document.getElementById('loginErr').style.display='flex'; return; } const btn = document.getElementById('loginBtn'); btn.disabled = true; btn.innerHTML = '
'; try { const res = await api('/api/admin/login','POST',{password:p}); adminToken = res.token; closeLoginModal(); document.getElementById('anavBadge').style.display = 'inline-flex'; document.getElementById('loutBtn').style.display = 'inline-flex'; document.getElementById('adminLoginBtn').style.display = 'none'; document.getElementById('tabAdmin').classList.add('show'); switchPanel('admin'); await loadAdminData(); } catch(e) { document.getElementById('loginErr').style.display = 'flex'; } btn.disabled = false; btn.innerHTML = ' Log Masuk'; } function doLogout() { adminToken = null; document.getElementById('anavBadge').style.display = 'none'; document.getElementById('loutBtn').style.display = 'none'; document.getElementById('adminLoginBtn').style.display = 'flex'; document.getElementById('tabAdmin').classList.remove('show','active'); document.getElementById('tabUser').classList.add('active'); switchPanel('user'); } // ─── SEARCH ─────────────────────────────────────────────── function debSearch() { clearTimeout(debTimer); debTimer = setTimeout(doSearch, 380); } async function doSearch() { const q = document.getElementById('si').value.trim(); const el = document.getElementById('sresults'); if (q.length < 2) { el.innerHTML = ''; return; } el.innerHTML = '

Mencari...

'; try { const rows = await api(`/api/search?q=${encodeURIComponent(q)}`); renderResults(rows); } catch(e) { el.innerHTML = '
Tidak dapat sambung. Cuba semula.
'; } } function renderResults(rows) { const el = document.getElementById('sresults'); if (!rows.length) { el.innerHTML = `
Tiada Rekod Dijumpai
Cuba cari dengan nama lain atau hubungi pihak sekolah.
`; return; } const fams = groupByFamily(rows); let html = ''; let delay = 0; for (const [fam, mem] of Object.entries(fams)) { const allPaid = mem.every(m => m.paid); const total = mem.reduce((a,m) => a+m.fee, 0); html += `

${fam}

${allPaid?'✓ Sudah Bayar':'✗ Belum Bayar'}
`; mem.forEach(m => { html += `
${m.name.charAt(0)}
${m.name}
Kelas ${m.class}
RM ${parseFloat(m.fee).toFixed(2)}
`; }); html += `
Jumlah (${mem.length} murid) RM ${total.toFixed(2)}
`; if (allPaid) { html += `
Pembayaran Telah Disahkan
Tarikh: ${mem[0].pay_date||'-'}
`; } else { html += `
`; } html += `
`; delay += 60; } el.innerHTML = html; } // ─── PAYMENT ────────────────────────────────────────────── function goToPay(mem) { curFam = mem[0].family_name; const total = mem.reduce((a,m)=>a+m.fee,0); payData = { mem, total }; document.getElementById('payFamLabel').textContent = curFam + ' — RM ' + total.toFixed(2); document.getElementById('payTotalDisp').textContent = 'RM ' + total.toFixed(2); document.getElementById('tickAmtTxt').textContent = 'RM ' + total.toFixed(2); document.getElementById('bkLbl').textContent = settings.bank_name || '—'; document.getElementById('bkNum').textContent = settings.bank_number || '—'; document.getElementById('bkHolder').textContent = settings.bank_holder || '—'; let cf = ''; mem.forEach((m,i) => { cf += `
`; }); document.getElementById('childFields').innerHTML = cf; document.getElementById('pParent').value = ''; document.getElementById('pPhone').value = mem[0].phone || ''; document.getElementById('payTick').checked = false; document.getElementById('waSendBtn').disabled = true; document.getElementById('tickBox').className = 'tick-box'; document.getElementById('payFormCard').style.display = 'block'; document.getElementById('step3').style.display = 'none'; setStep(1); showScreen('pay'); window.scrollTo({ top: 0, behavior: 'smooth' }); } function setStep(n) { for (let i=1;i<=3;i++) { const dot = document.getElementById('sd'+i); const lbl = document.getElementById('sl'+i); if (i{ const btn = document.querySelector('.copy-btn'); btn.innerHTML = ' Disalin!'; setTimeout(()=> btn.innerHTML=' Salin', 2000); }); } async function sendToWA() { const { mem, total, parent, phone } = payData; const wa = (settings.treasurer_phone||'').replace(/\D/g,''); const today = new Date().toISOString().slice(0,10); const rn = 'PIBG-' + Date.now().toString().slice(-6); const childList = mem.map((m,i)=>`${i+1}. ${m.name} (Kelas ${m.class})`).join('\n'); const msg = encodeURIComponent( `*NOTIS PEMBAYARAN YURAN PIBG ${settings.school_year||''}*\n*${settings.school_name||''}*\n\n`+ `No. Rujukan: ${rn}\nTarikh: ${today}\n\n`+ `*Maklumat Pembayar:*\nIbu/Bapa: ${parent}\nNo. Tel: ${phone}\nKeluarga: ${curFam}\n\n`+ `*Senarai Anak:*\n${childList}\n\n`+ `*Jumlah Dibayar: RM ${total.toFixed(2)}*\nAkaun: ${settings.bank_number} (${settings.bank_name})\n\n`+ `Sila sahkan penerimaan bayaran ini. Terima kasih.` ); window.open(`https://wa.me/${wa}?text=${msg}`, '_blank'); try { await api('/api/payment/notify','POST',{family_name:curFam, phone}); } catch(e){} document.getElementById('payFormCard').style.display = 'none'; document.getElementById('step3').style.display = 'block'; let rows = mem.map(m=>`
${m.name} (${m.class})RM ${parseFloat(m.fee).toFixed(2)}
`).join(''); document.getElementById('sucBox').innerHTML = rows + `
JumlahRM ${total.toFixed(2)}
Ref: ${rn} | ${today}
`; document.getElementById('si').value = ''; document.getElementById('sresults').innerHTML = ''; } // ─── ADMIN DATA ─────────────────────────────────────────── async function loadAdminData() { try { const [sts, cfg] = await Promise.all([api('/api/admin/students'), api('/api/admin/settings')]); allStudents = sts; settings = cfg; applyPublicSettings(); loadSettingsForm(); } catch(e) { console.error(e); } } function aSection(sec) { ['Dashboard','Students','Payments','Import','Settings'].forEach(s=>{ const el=document.getElementById('sec'+s); if(el) el.style.display='none'; }); document.getElementById('sec'+sec.charAt(0).toUpperCase()+sec.slice(1)).style.display='block'; document.querySelectorAll('.sidebar-item').forEach(i=>i.classList.remove('active')); const idx={dashboard:0,students:1,payments:2,import:3,settings:4}; document.querySelectorAll('.sidebar-item')[idx[sec]].classList.add('active'); if (sec==='dashboard') renderDashboard(); if (sec==='students') renderST(); if (sec==='payments') renderPayTable(); if (sec==='settings') loadSettingsForm(); } async function renderDashboard() { try { const s = await api('/api/admin/stats'); document.getElementById('statsRow').innerHTML = `
👨‍👩‍👧
Jumlah Murid
${s.total_students}
Semua murid berdaftar
✅
Keluarga Bayar
${s.paid_families}
daripada ${s.total_families} keluarga
💰
Kutipan
RM ${s.paid_amount}
daripada RM ${s.total_amount}
⏳
Belum Bayar
${s.total_families-s.paid_families}
keluarga
`; const fams = groupByFamily(allStudents); let html=''; for (const [f,mem] of Object.entries(fams)) { if (mem.every(m=>m.paid)) { const t=mem.reduce((a,m)=>a+m.fee,0); html+=`${f}${mem.length}RM ${t}Sudah Bayar${mem[0].pay_date||'-'}`; } } if (!html) html='Tiada rekod pembayaran lagi'; document.getElementById('recPayTbl').innerHTML = html; } catch(e){} } function renderST() { const q=(document.getElementById('asearch')||{value:''}).value.toLowerCase(); const fs=(document.getElementById('fstat')||{value:''}).value; const list=allStudents.filter(s=>{ const mq=!q||s.name.toLowerCase().includes(q)||s.family_name.toLowerCase().includes(q)||s.class.toLowerCase().includes(q); const ms=!fs||(fs==='paid'?s.paid:!s.paid); return mq&&ms; }); let html=''; list.forEach(s=>{ html+=`${s.name}${s.class}${s.family_name}RM ${s.fee} ${s.paid?'Sudah Bayar':'Belum Bayar'}
`; }); if (!html) html='Tiada rekod'; document.getElementById('stBody').innerHTML = html; } function renderPayTable() { const fams = groupByFamily(allStudents); let html=''; for (const [fam,mem] of Object.entries(fams)) { const allPaid=mem.every(m=>m.paid), total=mem.reduce((a,m)=>a+m.fee,0), phone=mem[0].phone||'-'; html+=` ${fam}
${mem.map(m=>m.name).join(', ')} ${phone}RM ${total} ${allPaid?'Sudah Bayar':'Belum Bayar'}
${allPaid?``:''}
`; } document.getElementById('payBody').innerHTML = html; } async function togglePaid(id, paid, fam) { try { await api(`/api/admin/students/${id}/paid`,'PATCH',{paid}); allStudents.forEach(s=>{ if(s.family_name===fam){s.paid=paid;s.pay_date=paid?new Date().toISOString().slice(0,10):null;} }); renderPayTable(); const el=document.getElementById('payAlertA'); el.innerHTML=`
${fam} ditandakan ${paid?'sudah':'belum'} bayar.
`; setTimeout(()=>el.innerHTML='',3000); } catch(e){ alert('Ralat: '+e.message); } } function waAdmin(phone, fam) { const n=phone.replace(/\D/g,''); const num=n.startsWith('0')?'6'+n:n; const msg=encodeURIComponent(`Salam ${fam}, ini peringatan yuran PIBG ${settings.school_year||''}. Sila hubungi kami. Terima kasih.`); window.open(`https://wa.me/${num}?text=${msg}`,'_blank'); } // ─── RECEIPT ────────────────────────────────────────────── function openRec(fam) { curRecFam = fam; const mem=allStudents.filter(s=>s.family_name===fam); const total=mem.reduce((a,m)=>a+m.fee,0); const pd=mem[0].pay_date||new Date().toISOString().slice(0,10); const rn='PIBG-'+Date.now().toString().slice(-6); let rows=mem.map(m=>`
${m.name} (${m.class})RM ${parseFloat(m.fee).toFixed(2)}
`).join(''); document.getElementById('recPreview').innerHTML=`
PIBG ${settings.school_name||''}
RESIT RASMI YURAN PIBG ${settings.school_year||''}
No. Resit: ${rn}
Keluarga: ${fam}
Tarikh: ${pd}
No. Tel: ${mem[0].phone||'-'}
${rows}
JUMLAHRM ${total.toFixed(2)}
Terima kasih atas sokongan kepada PIBG.
`; document.getElementById('recModal').classList.add('open'); } function closeRec() { document.getElementById('recModal').classList.remove('open'); } function sendRecWA() { const mem=allStudents.filter(s=>s.family_name===curRecFam); const total=mem.reduce((a,m)=>a+m.fee,0); const ph=(mem[0].phone||'').replace(/\D/g,''); const num=ph.startsWith('0')?'6'+ph:ph; const rn='PIBG-'+Date.now().toString().slice(-6); const msg=encodeURIComponent( `*RESIT YURAN PIBG ${settings.school_year||''}*\n*${settings.school_name||''}*\n\n`+ `No. Resit: ${rn}\nKeluarga: ${curRecFam}\nJumlah: RM ${total.toFixed(2)}\nTarikh: ${mem[0].pay_date||'-'}\n\n`+ `Terima kasih kerana membayar yuran PIBG. Sokongan anda amat dihargai.` ); window.open(`https://wa.me/${num}?text=${msg}`,'_blank'); closeRec(); } function dlPDF() { if (!window.jspdf) { alert('PDF tidak tersedia.'); return; } const {jsPDF}=window.jspdf; const doc=new jsPDF({unit:'mm',format:[80,120]}); const mem=allStudents.filter(s=>s.family_name===curRecFam); const total=mem.reduce((a,m)=>a+m.fee,0); const pd=mem[0].pay_date||new Date().toISOString().slice(0,10); const rn='PIBG-'+Date.now().toString().slice(-6); doc.setFontSize(11);doc.setFont('helvetica','bold'); doc.text('PIBG '+(settings.school_name||''),40,10,{align:'center'}); doc.setFontSize(8);doc.setFont('helvetica','normal'); doc.text('RESIT RASMI YURAN PIBG '+(settings.school_year||''),40,15,{align:'center'}); doc.text('No. Resit: '+rn,40,19,{align:'center'}); doc.setLineWidth(.5);doc.line(5,21,75,21); doc.text('Keluarga: '+curRecFam,5,26);doc.text('Tarikh: '+pd,5,30);doc.text('No. Tel: '+(mem[0].phone||'-'),5,34); doc.line(5,36,75,36); let y=41; mem.forEach(m=>{doc.text(m.name+' ('+m.class+')',5,y);doc.text('RM '+parseFloat(m.fee).toFixed(2),70,y,{align:'right'});y+=5;}); doc.setLineWidth(.8);doc.line(5,y,75,y);y+=5; doc.setFont('helvetica','bold');doc.text('JUMLAH',5,y);doc.text('RM '+total.toFixed(2),70,y,{align:'right'}); y+=8;doc.setFont('helvetica','normal');doc.setFontSize(7); doc.text('Terima kasih atas sokongan kepada PIBG.',40,y,{align:'center'}); doc.save('Resit-PIBG-'+curRecFam+'.pdf'); } // ─── STUDENT CRUD ────────────────────────────────────────── function openAdd() { editId=null; document.getElementById('mtit').innerHTML=' Tambah Murid'; ['fN','fC','fF','fPh'].forEach(id=>document.getElementById(id).value=''); document.getElementById('fFee').value=settings.default_fee||50; document.getElementById('stuModal').classList.add('open'); } function openEdit(id) { const s=allStudents.find(x=>x.id===id); if(!s) return; editId=id; document.getElementById('mtit').innerHTML=' Edit Murid'; document.getElementById('fN').value=s.name; document.getElementById('fC').value=s.class; document.getElementById('fF').value=s.family_name; document.getElementById('fFee').value=s.fee; document.getElementById('fPh').value=s.phone||''; document.getElementById('stuModal').classList.add('open'); } function closeModal() { document.getElementById('stuModal').classList.remove('open'); } async function saveStu() { const name=document.getElementById('fN').value.trim(); const cls=document.getElementById('fC').value.trim(); const fam=document.getElementById('fF').value.trim(); const fee=parseFloat(document.getElementById('fFee').value)||50; const phone=document.getElementById('fPh').value.trim(); if (!name||!cls||!fam) { alert('Sila isi semua maklumat wajib.'); return; } const btn=document.getElementById('saveBtn'); btn.disabled=true; try { if (editId) { const s=allStudents.find(x=>x.id===editId); await api(`/api/admin/students/${editId}`,'PUT',{name,class:cls,family_name:fam,fee,phone,paid:s.paid?1:0,pay_date:s.pay_date}); Object.assign(s,{name,class:cls,family_name:fam,fee,phone}); showAlt('Data murid berjaya dikemaskini.',true); } else { const res=await api('/api/admin/students','POST',{name,class:cls,family_name:fam,fee,phone}); allStudents.push({id:res.id,name,class:cls,family_name:fam,fee,phone,paid:false,pay_date:null}); showAlt('Murid baru berjaya ditambah.',true); } closeModal(); renderST(); } catch(e) { alert('Ralat: '+e.message); } btn.disabled=false; } async function delStu(id) { if (!confirm('Padam rekod murid ini?')) return; try { await api(`/api/admin/students/${id}`,'DELETE'); allStudents=allStudents.filter(s=>s.id!==id); renderST(); showAlt('Rekod murid telah dipadam.',false); } catch(e) { alert('Ralat: '+e.message); } } function showAlt(msg,ok) { const el=document.getElementById('alertA'); el.innerHTML=`
${msg}
`; setTimeout(()=>el.innerHTML='',3000); } // ─── SETTINGS ────────────────────────────────────────────── function loadSettingsForm() { document.getElementById('setPhone').value = settings.treasurer_phone||''; document.getElementById('setBkName').value = settings.bank_name||''; document.getElementById('setBkNum').value = settings.bank_number||''; document.getElementById('setBkHolder').value = settings.bank_holder||''; document.getElementById('setSkName').value = settings.school_name||''; document.getElementById('setSkYear').value = settings.school_year||''; document.getElementById('setSkFee').value = settings.default_fee||'50'; } async function saveSettings() { const u={ treasurer_phone:document.getElementById('setPhone').value.trim(), bank_name:document.getElementById('setBkName').value.trim(), bank_number:document.getElementById('setBkNum').value.trim(), bank_holder:document.getElementById('setBkHolder').value.trim(), school_name:document.getElementById('setSkName').value.trim(), school_year:document.getElementById('setSkYear').value.trim(), default_fee:document.getElementById('setSkFee').value.trim(), }; try { await api('/api/admin/settings','PUT',u); Object.assign(settings,u); applyPublicSettings(); const el=document.getElementById('settingAlert'); el.innerHTML='
Tetapan berjaya disimpan.
'; setTimeout(()=>el.innerHTML='',3000); } catch(e){ alert('Ralat: '+e.message); } } // ─── IMPORT/EXPORT ───────────────────────────────────────── async function handleCSV(e) { const file=e.target.files[0]; if(!file) return; const reader=new FileReader(); reader.onload=async function(ev){ const lines=ev.target.result.split('\n').filter(l=>l.trim()); let added=0; for (const line of lines.slice(1)) { const p=line.split(',').map(x=>x.trim().replace(/"/g,'')); if (p.length>=3) { try { const res=await api('/api/admin/students','POST',{name:p[0],class:p[1],family_name:p[2],fee:parseFloat(p[3])||50,phone:p[4]||''}); allStudents.push({id:res.id,name:p[0],class:p[1],family_name:p[2],fee:parseFloat(p[3])||50,phone:p[4]||'',paid:false,pay_date:null}); added++; } catch(e){} } } document.getElementById('upRes').innerHTML=`
${added} rekod berjaya diimport.
`; setTimeout(()=>document.getElementById('upRes').innerHTML='',4000); }; reader.readAsText(file); } function toCSV(data){ const h='Nama,Kelas,Keluarga,Yuran,Telefon,Status,Tarikh Bayar'; return[h,...data.map(s=>`"${s.name}","${s.class}","${s.family_name}",${s.fee},"${s.phone||''}","${s.paid?'Sudah Bayar':'Belum Bayar'}","${s.pay_date||''}"`)].join('\n'); } function dlCSV(c,fn){const b=new Blob([c],{type:'text/csv'});const a=document.createElement('a');a.href=URL.createObjectURL(b);a.download=fn;a.click();} function expAll() { dlCSV(toCSV(allStudents),'yuran-pibg-semua.csv'); } function expPaid() { dlCSV(toCSV(allStudents.filter(s=>s.paid)),'yuran-pibg-sudah-bayar.csv'); } function expUnpaid(){ dlCSV(toCSV(allStudents.filter(s=>!s.paid)),'yuran-pibg-belum-bayar.csv'); } // ─── START ──────────────────────────────────────────────── init();