Tolak Penghantaran
Ketersediaan Hari Ini
Saya Tersedia untuk Urgent
Bayaran ×1.7 — Prioriti tinggi, hantar dalam masa beberapa jam
Thread Aduan
#——
Menunggu…
Memuatkan thread…
// ── COD REMITTANCE ─────────────────────────────────────────────── let _codRemitModal=null; function openCodRemitModal(){ if(!_codRemitModal){ const m=document.createElement('div'); m.id='codRemitOverlay'; m.style.cssText='position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.5);display:flex;align-items:flex-end'; m.innerHTML=`
REMITTANCE COD
Hantar 30% daripada jumlah COD yang anda kumpul kepada SEMUA. Ini adalah bahagian platform daripada setiap pesanan COD.
Admin akan mengesahkan penerimaan dalam 24 jam
`; document.body.appendChild(m); _codRemitModal=m; }else{ _codRemitModal.style.display='flex'; document.getElementById('codRemitAmount').value=''; document.getElementById('codRemitNote').value=''; const errDiv=document.getElementById('codRemitError'); if(errDiv)errDiv.style.display='none'; } } function closeCodRemitModal(){ if(_codRemitModal)_codRemitModal.style.display='none'; } async function submitCodRemit(){ const amount=parseFloat(document.getElementById('codRemitAmount')?.value); const note=document.getElementById('codRemitNote')?.value?.trim()||null; const errDiv=document.getElementById('codRemitError'); const showErr=msg=>{if(errDiv){errDiv.textContent=msg;errDiv.style.display='block';}}; if(!amount||amount<0.01)return showErr('Sila masukkan jumlah yang sah.'); // Get recent COD order IDs for reference let orderIds=[]; try{ const{data:codOrders}=await sb.from('orders') .select('id').eq('runner_id',currentRunner.id) .eq('payment_method','cash').eq('cod_status','collected') .order('cod_collected_at',{ascending:false}).limit(20); orderIds=(codOrders||[]).map(o=>o.id); }catch(e){} try{ const{error}=await sb.from('cod_remittances').insert({ runner_id:currentRunner.id, amount, order_ids:orderIds, status:'pending', admin_notes:note }); if(error)throw error; closeCodRemitModal(); // Show toast const t=document.createElement('div'); t.style.cssText='position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:var(--mint);color:white;padding:12px 20px;border-radius:12px;font-size:.8rem;font-weight:700;z-index:9999;box-shadow:0 4px 20px rgba(0,0,0,.2)'; t.textContent=' Remittance dihantar! Menunggu pengesahan admin.'; document.body.appendChild(t);setTimeout(()=>t.remove(),4000); }catch(e){showErr('Gagal: '+e.message);} } // ── DELIVERY PROOF MODAL ───────────────────────────────────────────────────── let _proofOrderId=null; let _proofModal=null; async function openDeliveryProofModal(orderId){ _proofOrderId=orderId; if(!_proofModal){ const m=document.createElement('div'); m.id='proofOverlay'; m.style.cssText='position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.6);display:flex;align-items:flex-end'; m.innerHTML=`
BUKTI PENGHANTARAN
Ambil gambar bungkusan yang telah dihantar atau bukti di pintu pembeli. Ini melindungi anda daripada tuntutan tidak diterima.
`; document.body.appendChild(m); _proofModal=m; }else{ // Reset _proofModal.style.display='flex'; document.getElementById('proofImgPreview').style.display='none'; document.getElementById('proofFileInput').value=''; document.getElementById('proofPickBtn').textContent=' Ambil / Pilih Foto'; document.getElementById('proofSubmitBtn').disabled=true; document.getElementById('proofSubmitBtn').style.opacity='.5'; document.getElementById('proofUploadStatus').style.display='none'; } } function closeDeliveryProofModal(){ if(_proofModal)_proofModal.style.display='none'; } function onProofFileSelected(input){ const file=input.files?.[0]; if(!file)return; const reader=new FileReader(); reader.onload=e=>{ const img=document.getElementById('proofImg'); const prev=document.getElementById('proofImgPreview'); const btn=document.getElementById('proofSubmitBtn'); const pickBtn=document.getElementById('proofPickBtn'); if(img)img.src=e.target.result; if(prev)prev.style.display='block'; if(btn){btn.disabled=false;btn.style.opacity='1';} if(pickBtn)pickBtn.textContent=' Tukar Foto'; }; reader.readAsDataURL(file); } async function submitWithProof(){ const input=document.getElementById('proofFileInput'); const file=input?.files?.[0]; if(!file){submitWithoutProof();return;} const statusEl=document.getElementById('proofUploadStatus'); const submitBtn=document.getElementById('proofSubmitBtn'); if(statusEl)statusEl.style.display='block'; if(submitBtn){submitBtn.disabled=true;submitBtn.textContent='Memuat naik...';} try{ // Compress image before upload let uploadFile=file; try{uploadFile=await compressImageRunner(file,1600,0.85);} catch(ce){uploadFile=file;} const path=`${currentRunner.id}/${_proofOrderId}-${Date.now()}.jpg`; const{error:upErr}=await sb.storage.from('delivery-proofs') .upload(path,uploadFile,{contentType:'image/jpeg',upsert:false}); if(upErr)throw upErr; if(statusEl)statusEl.style.display='none'; await _executeDelivered(_proofOrderId, path); }catch(e){ if(statusEl)statusEl.style.display='none'; if(submitBtn){submitBtn.disabled=false;submitBtn.textContent=' Hantar Dengan Foto';} alert('Gagal muat naik bukti: '+e.message); } } async function submitWithoutProof(){ if(!confirm('Teruskan tanpa foto bukti? Anda mungkin tidak dilindungi jika pembeli membuat aduan.'))return; closeDeliveryProofModal(); await _executeDelivered(_proofOrderId, null); } // Canvas compression for runner (no external dependency) function compressImageRunner(file,maxPx=1600,quality=0.85){ return new Promise((resolve,reject)=>{ const img=new Image(); const url=URL.createObjectURL(file); img.onload=()=>{ URL.revokeObjectURL(url); let{width:w,height:h}=img; if(w>maxPx||h>maxPx){ if(w>h){h=Math.round(h*maxPx/w);w=maxPx;} else{w=Math.round(w*maxPx/h);h=maxPx;} } const c=document.createElement('canvas'); c.width=w;c.height=h; c.getContext('2d').drawImage(img,0,0,w,h); c.toBlob(b=>{ if(!b)return reject(new Error('Canvas empty')); resolve(new File([b],file.name.replace(/\.[^.]+$/,'.jpg'),{type:'image/jpeg'})); },'image/jpeg',quality); }; img.onerror=()=>reject(new Error('Load failed')); img.src=url; }); }
NOTIFIKASI
Tiada notifikasi
// ── RUNNER NOTIFICATION SYSTEM ───────────────────────────────────────────── let _runnerNotifs = []; let _runnerNotifPoll = null; let _runnerNotifRealtime = null; async function loadRunnerNotifications(){ if(!currentRunner) return; try{ const{data} = await sb.from('notifications') .select('*') .eq('user_id', currentRunner.id) .order('created_at', {ascending:false}) .limit(30); _runnerNotifs = data || []; updateRunnerBellBadge(); renderRunnerNotifs(); }catch(e){ console.warn('Runner notifs:', e); } } function updateRunnerBellBadge(){ const unread = _runnerNotifs.filter(n => !n.is_read).length; const badge = document.getElementById('runnerBellBadge'); if(badge){ badge.textContent = unread > 9 ? '9+' : unread; badge.style.display = unread > 0 ? 'flex' : 'none'; } } function renderRunnerNotifs(){ const list = document.getElementById('runnerNotifList'); if(!list) return; if(!_runnerNotifs.length){ list.innerHTML = '
Tiada notifikasi
'; return; } const typeIcon = { order_new: '', order_update: '', delivery_update: '', dispute: '', system: '', }; list.innerHTML = _runnerNotifs.map(n => `
${typeIcon[n.type]||typeIcon.system}
${n.title||'Notifikasi'}
${n.message||n.body||''}
${timeAgo(n.created_at)}
${!n.is_read?'
':''}
`).join(''); } function openRunnerNotifPanel(){ const panel = document.getElementById('runnerNotifPanel'); if(panel) panel.classList.add('open'); loadRunnerNotifications(); } function closeRunnerNotifPanel(){ const panel = document.getElementById('runnerNotifPanel'); if(panel) panel.classList.remove('open'); } async function markAllRunnerNotifsRead(){ if(!currentRunner||!_runnerNotifs.length) return; const unreadIds = _runnerNotifs.filter(n=>!n.is_read).map(n=>n.id); if(!unreadIds.length) return; try{ await sb.from('notifications').update({is_read:true}).in('id',unreadIds); _runnerNotifs.forEach(n=>n.is_read=true); updateRunnerBellBadge(); renderRunnerNotifs(); }catch(e){ console.warn('Mark all read:', e); } } async function tapRunnerNotif(notifId, relatedId){ // Mark as read try{ await sb.from('notifications').update({is_read:true}).eq('id',notifId); const n = _runnerNotifs.find(x=>x.id===notifId); if(n) n.is_read = true; updateRunnerBellBadge(); }catch(e){} closeRunnerNotifPanel(); // If related to an order, switch to active tab if(relatedId){ switchTab('active', document.getElementById('tabActive')); } } function startRunnerNotifRealtime(){ if(!currentRunner) return; if(_runnerNotifRealtime) sb.removeChannel(_runnerNotifRealtime); _runnerNotifRealtime = sb.channel('runner-notifs-'+currentRunner.id) .on('postgres_changes',{ event:'INSERT', schema:'public', table:'notifications', filter:'user_id=eq.'+currentRunner.id }, payload=>{ _runnerNotifs.unshift(payload.new); updateRunnerBellBadge(); // Show brief toast for new order notifications if(payload.new.type==='order_update' || payload.new.type==='order_new'){ showRunnerToast(payload.new.title || 'Notifikasi baru'); } }) .subscribe(); // Poll every 60s as fallback if(_runnerNotifPoll) clearInterval(_runnerNotifPoll); _runnerNotifPoll = setInterval(loadRunnerNotifications, 60000); } function showRunnerToast(msg){ const t = document.createElement('div'); t.style.cssText = 'position:fixed;top:70px;left:50%;transform:translateX(-50%);background:var(--mint);color:#0a0a0a;padding:10px 18px;border-radius:12px;font-size:.78rem;font-weight:700;z-index:9999;box-shadow:0 4px 20px rgba(0,0,0,.25);max-width:280px;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis'; t.textContent = msg; document.body.appendChild(t); setTimeout(()=>t.remove(), 3500); }