{"id":476,"date":"2025-05-23T04:43:27","date_gmt":"2025-05-23T04:43:27","guid":{"rendered":"https:\/\/abatablaster.xyz\/?page_id=476"},"modified":"2025-08-27T16:59:00","modified_gmt":"2025-08-27T16:59:00","slug":"broadcast-dashboard","status":"publish","type":"page","link":"https:\/\/abatablaster.xyz\/index.php\/broadcast-dashboard\/","title":{"rendered":"Broadcast Dashboard"},"content":{"rendered":"\n<!DOCTYPE html>\n<html lang=\"ms\">\n\n<head>\n    <meta charset=\"UTF-8\" \/>\n    <title>Dashboard Broadcast<\/title>\n    <script src=\"https:\/\/www.gstatic.com\/firebasejs\/9.22.2\/firebase-app-compat.js\"><\/script>\n    <script src=\"https:\/\/www.gstatic.com\/firebasejs\/9.22.2\/firebase-auth-compat.js\"><\/script>\n    <script src=\"https:\/\/www.gstatic.com\/firebasejs\/9.22.2\/firebase-firestore-compat.js\"><\/script>\n    <script src=\"https:\/\/cdn.jsdelivr.net\/npm\/sweetalert2@11\"><\/script>\n    <style>\n        body {\n            font-family: Arial, sans-serif;\n            padding: 20px;\n            background: #f4f8f4;\n            color: #333;\n        }\n\n        .dropdown {\n            margin-bottom: 20px;\n        }\n\n        table {\n            border-collapse: separate;\n            \/* <-- penting utk sticky yang konsisten *\/\n            border-spacing: 0;\n            \/* kekalkan rupa macam collapse *\/\n            width: 100%;\n            background: #fff;\n        }\n\n        th,\n        td {\n            border: 1px solid #ccc;\n            padding: 8px;\n            text-align: left;\n            white-space: nowrap;\n        }\n\n        thead th {\n            background: #dcedc8;\n            position: sticky;\n            top: 0;\n            z-index: 3;\n        }\n\n        .table-container {\n            max-height: 400px;\n            overflow: auto;\n            \/* X + Y scroll dalam bekas ini *\/\n            position: relative;\n            \/* sticky th rujuk bekas ini *\/\n        }\n\n        .logo {\n            text-align: center;\n            margin-bottom: 20px;\n        }\n\n        .logo img {\n            max-height: 80px;\n            border-radius: 12px;\n        }\n\n        .filter-section {\n            margin-bottom: 20px;\n            background: #e8f5e9;\n            padding: 10px;\n            border-radius: 8px;\n        }\n\n        .status-menunggu {\n            background-color: #fff9c4;\n        }\n\n        \/* kuning lembut *\/\n        .status-sedang {\n            background-color: #bbdefb;\n        }\n\n        \/* biru lembut *\/\n        .status-selesai {\n            background-color: #c8e6c9;\n        }\n\n        \/* hijau lembut *\/\n        .status-gagal {\n            background-color: #ffcdd2;\n        }\n\n        \/* merah lembut *\/\n        .status-batal {\n            background-color: #eeeeee;\n        }\n\n        \/* kelabu *\/\n\n        .table-responsive {\n            overflow-x: auto;\n            width: 100%;\n        }\n\n        table,\n        th,\n        td {\n            font-size: 13px;\n        }\n\n        button {\n            cursor: pointer;\n        }\n\n        button[onclick^=\"padamKempen\"] {\n            background-color: #ef5350;\n            color: white;\n            border: none;\n            padding: 5px 10px;\n            border-radius: 5px;\n            font-size: 12px;\n        }\n\n        \/* badges jenis kempen *\/\n        .badge {\n            padding: 2px 8px;\n            border-radius: 999px;\n            font-weight: 700;\n            font-size: 11px;\n            display: inline-block\n        }\n\n        .badge-now {\n            background: #e3f2fd;\n            color: #0d47a1\n        }\n\n        .badge-scheduled {\n            background: #fff3e0;\n            color: #e65100\n        }\n\n        .badge-recurring {\n            background: #ede7f6;\n            color: #4527a0\n        }\n\n        .table-note {\n            margin-top: 6px;\n            font-size: 12px;\n            opacity: .75;\n        }\n    <\/style>\n<\/head>\n\n<body>\n    <div class=\"logo\">\n        <img decoding=\"async\" src=\"https:\/\/abatablaster.xyz\/wp-content\/uploads\/2025\/05\/photo_2024-05-16_15-47-59.jpg\"\n            alt=\"Logo AbataBlaster\" \/>\n    <\/div>\n    <h2>\ud83d\udcca Dashboard Kempen Broadcast<\/h2>\n\n    <div class=\"dropdown\">\n        <label for=\"pilihNombor\"><strong>Pilih Nombor WhatsApp:<\/strong><\/label><br \/>\n        <select id=\"pilihNombor\" style=\"padding: 5px 10px;\"><\/select>\n    <\/div>\n\n    <div class=\"filter-section\">\n        <label>Status: <\/label>\n        <select id=\"filterStatus\" style=\"margin-right:10px;\">\n            <option value=\"\">\u2013 Semua \u2013<\/option>\n            <option value=\"MENUNGGU\">MENUNGGU<\/option>\n            <option value=\"SEDANG_HANTAR\">SEDANG_HANTAR<\/option>\n            <option value=\"SELESAI\">SELESAI<\/option>\n            <option value=\"GAGAL\">GAGAL<\/option>\n            <option value=\"DIBATALKAN\">DIBATALKAN<\/option>\n        <\/select>\n\n        <label>Tarikh (YYYY-MM-DD): <\/label>\n        <input type=\"date\" id=\"filterTarikh\" \/>\n    <\/div>\n\n    <div id=\"senaraiKempen\" class=\"table-container\"><\/div>\n\n    <!-- nota statik (tak ikut scroll table) -->\n    <div id=\"notaGagal\" class=\"table-note\"><\/div>\n\n    <div id=\"jumlahKempen\" style=\"margin-top: 10px; font-size: 14px; font-weight: bold;\"><\/div>\n\n    <div style=\"margin-top: 20px;\">\n        <button onclick=\"pilihPadamSemua()\">\ud83d\uddd1\ufe0f Padam Kempen Mengikut Status<\/button>\n    <\/div>\n\n    <script>\n        \/\/ \u2705 Gantikan dengan konfigurasi Firebase projek anda\n        const firebaseConfig = {\n            apiKey: \"AIzaSyBou8nlJ7uPZ4ioOJapzC8Dn3-K7Qs-yco\",\n            authDomain: \"whatsapp-ai-saas.firebaseapp.com\",\n            projectId: \"whatsapp-ai-saas\",\n            storageBucket: \"whatsapp-ai-saas.appspot.com\",\n            messagingSenderId: \"287462007544\",\n            appId: \"1:287462007544:web:913db884edf906a37cd10d\"\n        };\n\n        firebase.initializeApp(firebaseConfig);\n        const auth = firebase.auth();\n        const db = firebase.firestore();\n\n        let currentUserUid = null;\n\n        function normPhone(s) { return String(s || '').replace(\/\\D\/g, '').replace(\/^0\/, '60'); }\n\n        auth.onAuthStateChanged(async (user) => {\n            if (!user) {\n                Swal.fire(\"Log Masuk Diperlukan\", \"Sila log masuk untuk akses dashboard ini\", \"warning\");\n                return;\n            }\n\n            currentUserUid = user.uid;\n\n            const snap = await db.collection(\"user_numbers\").doc(currentUserUid).get();\n            const userSnap = await db.collection(\"users\").doc(currentUserUid).get();\n\n            if (!snap.exists || !snap.data().nombor || !userSnap.exists) return;\n\n            let senarai = snap.data().nombor;\n            const dataUser = userSnap.data();\n            const addonNombor = dataUser.addon_whatsapp || 0;\n            const had = (dataUser.hadNombor || 1) + addonNombor;\n\n\n            senarai = senarai.slice(0, had);\n\n            const dropdown = document.getElementById(\"pilihNombor\");\n            dropdown.innerHTML = '<option value=\"\">-- Pilih nombor --<\/option>';\n\n            \/\/ --- Tukar senarai (string\/obj) kepada array objek { phone, nama }\n            let senaraiObj = [];\n            for (const item of senarai) {\n                if (typeof item === \"string\") {\n                    senaraiObj.push({ phone: item, nama: \"\" });\n                } else if (item && typeof item.phone === \"string\") {\n                    senaraiObj.push({ phone: item.phone, nama: item.nama || \"\" });\n                }\n            }\n\n            \/\/ --- Ambil status sambungan serentak\n            const statusMap = {};\n            await Promise.all(senaraiObj.map(async ({ phone }) => {\n                let status = \"TIDAK_BERSAMBUNG\";\n                try {\n                    const qrSnap = await db.collection(\"whatsapp_qr\").doc(phone).get();\n                    status = qrSnap.exists ? (qrSnap.data().status || \"TIDAK_BERSAMBUNG\") : \"TIDAK_BERSAMBUNG\";\n                } catch { }\n                statusMap[phone] = status;\n            }));\n\n            dropdown.innerHTML = '<option value=\"\">-- Pilih nombor --<\/option>';\n\n            senaraiObj.forEach(({ phone, nama }) => {\n                const status = statusMap[phone] || \"TIDAK_BERSAMBUNG\";\n                const emoji = status === \"BERSAMBUNG\" ? \"\ud83d\udfe2\" : \"\ud83d\udd34\";\n                const opt = document.createElement(\"option\");\n                opt.value = phone;\n                opt.textContent = `${phone}${nama ? \" \u2013 \" + nama : \"\"} ${emoji}`;\n                dropdown.appendChild(opt);\n            });\n\n\n            dropdown.addEventListener(\"change\", () => {\n                const selected = dropdown.value;\n                localStorage.setItem(\"dashNomborTerpilih\", selected); \/\/ \ud83e\udde0 ingat pilihan terakhir\n                if (selected) {\n                    loadKempenUntukNombor(selected);\n                } else {\n                    document.getElementById(\"senaraiKempen\").innerHTML = \"\";\n                }\n            });\n\n            \/\/ \u23f1 Auto-refresh bila tukar filter status\n            document.getElementById(\"filterStatus\").addEventListener(\"change\", () => {\n                const selected = document.getElementById(\"pilihNombor\").value;\n                if (selected) loadKempenUntukNombor(selected);\n            });\n\n            \/\/ \u23f1 Auto-refresh bila tukar filter tarikh\n            document.getElementById(\"filterTarikh\").addEventListener(\"change\", () => {\n                const selected = document.getElementById(\"pilihNombor\").value;\n                if (selected) loadKempenUntukNombor(selected);\n            });\n\n            const saved = localStorage.getItem(\"dashNomborTerpilih\");\n            const firstConnected = senaraiObj.find(x => statusMap[x.phone] === \"BERSAMBUNG\");\n\n            const defaultPhone =\n                (saved && senaraiObj.some(x => x.phone === saved)) ? saved\n                    : (firstConnected ? firstConnected.phone\n                        : (senaraiObj[0]?.phone || \"\"));\n\n            if (defaultPhone) {\n                dropdown.value = defaultPhone;\n                loadKempenUntukNombor(defaultPhone);\n            }\n        });\n\n        async function loadKempenUntukNombor(phone) {\n            const kempenDiv = document.getElementById(\"senaraiKempen\");\n            kempenDiv.innerHTML = \"<p>Loading kempen...<\/p>\";\n\n            const filterStatus = document.getElementById(\"filterStatus\").value;\n            const filterTarikh = document.getElementById(\"filterTarikh\").value;\n\n            \/\/ \u2705 tapis ikut pemilik\n            let query = db.collection(\"campaigns\")\n                .where(\"uid\", \"==\", currentUserUid)\n                .where(\"pengirim\", \"==\", phone);\n\n            if (filterStatus) {\n                query = query.where(\"status\", \"==\", filterStatus);\n            }\n\n            \/\/ Ambil lebih sikit, tapis di client supaya support Timestamp ATAU string ISO\n            const snapshot = await query.orderBy(\"timestamp\", \"desc\").limit(200).get();\n\n            \/\/ Tapis di client jika user pilih tarikh\n            let docs = snapshot.docs;\n            if (filterTarikh) {\n                const start = new Date(filterTarikh); start.setHours(0, 0, 0, 0);\n                const end = new Date(filterTarikh); end.setHours(23, 59, 59, 999);\n\n                docs = docs.filter(d => {\n                    const ts = d.data().timestamp;\n                    const dt = ts?.toDate ? ts.toDate()\n                        : (typeof ts === \"string\" ? new Date(ts) : null);\n                    return dt && !isNaN(dt) && dt >= start && dt <= end;\n                });\n            }\n\n            \/* \u2014\u2014\u2014 Pastikan paling terbaru di atas walaupun selepas client-side filter \u2014\u2014\u2014 *\/\n            docs.sort((a, b) => {\n                const toMs = (doc) => {\n                    const ts = doc.data().timestamp;\n                    const dt = ts?.toDate ? ts.toDate()\n                        : (typeof ts === 'string' ? new Date(ts) : null);\n                    return (dt && !isNaN(dt)) ? dt.getTime() : 0;\n                };\n                return toMs(b) - toMs(a); \/\/ desc\n            });\n\n            if (docs.length === 0) {\n                kempenDiv.innerHTML = \"<p>Tiada kempen dijumpai untuk filter ini.<\/p>\";\n                return;\n            }\n\n            let html = `<table><thead>\n              <tr>\n                <th>ID Kempen<\/th>\n                <th>Nama Kempen<\/th>\n                <th>Jenis<\/th>               <!-- \ud83c\udd95 -->\n                <th>Status<\/th>\n                <th>Tarikh<\/th>\n                <th>Jumlah Nombor<\/th>\n                <th>Berjaya<\/th>\n                <th>Gagal<\/th>\n                <th>Bukan WhatsApp<\/th>      <!-- \ud83c\udd95 -->\n                <th>Leads Reply<\/th>\n                <th>Padam<\/th>\n              <\/tr>\n            <\/thead><tbody>`;\n\n\n            \/\/ \u23f3 Simpan senarai yang perlu update\n            const perluUpdate = [];\n\n            for (const doc of docs) {\n                const data = doc.data();\n                const id = doc.id;\n                const uid = data.uid || \"-\";\n                const nama = data.campaignName || data.nama || \"(Tiada nama)\";\n\n                const status = data.status || \"-\";\n                let tarikhKempen = \"-\";\n                const tsRaw = data.timestamp;\n                const dt = tsRaw?.toDate ? tsRaw.toDate() : (typeof tsRaw === \"string\" ? new Date(tsRaw) : null);\n                if (dt && !isNaN(dt)) {\n                    tarikhKempen = dt.toLocaleString(\"ms-MY\", {\n                        day: \"2-digit\", month: \"short\", year: \"numeric\",\n                        hour: \"2-digit\", minute: \"2-digit\"\n                    });\n                }\n\n                const jumlah = Array.isArray(data.numbers) ? data.numbers.length\n                    : (Array.isArray(data.senarai) ? data.senarai.length : 0);\n                const berjaya = data.jumlahBerjaya || 0;\n                const gagal = data.jumlahGagal || 0;\n\n                let reply = typeof data.jumlahReply === \"number\" ? data.jumlahReply : 0;\n\n                \/\/ \ud83e\udde0 Auto kira reply dari koleksi broadcast_leads\n                const startLead = (dt && !isNaN(dt)) ? dt : new Date(0);\n                const targetNos = Array.isArray(data.numbers) ? data.numbers : (Array.isArray(data.senarai) ? data.senarai : []);\n                const targetSet = new Set(targetNos.map(normPhone)); \/\/ \ud83d\udd27 normalise dulu\n\n                const leadsSnap = await db.collection(\"broadcast_leads\")\n                    .where(\"source\", \"==\", phone)          \/\/ kekalkan format sedia ada di DB\n                    .where(\"replyAt\", \">=\", startLead)\n                    .get();\n\n                let replyBaru = 0;\n                leadsSnap.forEach(d => {\n                    if (targetSet.has(normPhone(d.id))) replyBaru++;\n                });\n\n                if (replyBaru !== reply) {\n                    perluUpdate.push({ id, reply: replyBaru });\n                    reply = replyBaru;\n                }\n\n                const statusClass = {\n                    \"MENUNGGU\": \"status-menunggu\",\n                    \"SEDANG_HANTAR\": \"status-sedang\",\n                    \"SELESAI\": \"status-selesai\",\n                    \"GAGAL\": \"status-gagal\",\n                    \"DIBATALKAN\": \"status-batal\"\n                }[status] || \"\";\n\n                \/\/ \u2014\u2014\u2014 Jenis Kempen & Badge \u2014\u2014\u2014\n                const jenisKempen = (() => {\n                    const t = (data.schedule && data.schedule.type) || '';\n                    if (t === 'recurring') return 'Recurring';\n                    if (t === 'manual') return 'Scheduled';\n                    if (t === 'now') return 'Now';\n                    if (data.status === 'SCHEDULED') return 'Scheduled'; \/\/ fallback\n                    return 'Now';\n                })();\n                function jenisBadge(t) {\n                    const cls = t === 'Recurring' ? 'badge-recurring'\n                        : t === 'Scheduled' ? 'badge-scheduled'\n                            : 'badge-now';\n                    return `<span class=\"badge ${cls}\">${t}<\/span>`;\n                }\n\n                \/\/ \u2014\u2014\u2014 Pecahan Gagal vs Bukan WhatsApp \u2014\u2014\u2014\n                const gagalIncl = (data.jumlahGagal ?? data.failCount ?? 0);\n                const bwStored = (typeof data.jumlahBukanWhatsapp === 'number') ? data.jumlahBukanWhatsapp : null;\n                const gagalMurni = (bwStored != null) ? Math.max(0, gagalIncl - bwStored) : null;\n\n                html += `<tr class=\"${statusClass}\" data-rowid=\"${id}\">\n                  <td>${id}<\/td>\n                  <td>${nama}<\/td>\n                  <td>${jenisBadge(jenisKempen)}<\/td>  <!-- \ud83c\udd95 Jenis -->\n                  <td>${status}<\/td>\n                  <td>${tarikhKempen}<\/td>\n                  <td>${jumlah}<\/td>\n                  <td>${berjaya}<\/td>\n                  <td class=\"col-gagal\" data-id=\"${id}\">\n                    ${gagalMurni != null ? gagalMurni : `${gagalIncl}*`}  <!-- \ud83c\udd95 Gagal murni \/ sementara -->\n                  <\/td>\n                  <td class=\"col-bukanwa\" data-id=\"${id}\">\n                    ${bwStored != null ? bwStored : `<button class=\"btn-kira\" data-id=\"${id}\">\ud83d\udd0d Kira<\/button>`} <!-- \ud83c\udd95 Bukan WA -->\n                  <\/td>\n                  <td>${reply} <button onclick=\"paparLeads('${id}', \\`${nama}\\`)\">\ud83d\udcec Papar<\/button><\/td>\n                  <td><button onclick=\"padamKempen('${id}', \\`${nama}\\`)\">\u274c Padam<\/button><\/td>\n                <\/tr>`;\n            }\n\n            html += `<\/tbody><\/table>`;\n            kempenDiv.innerHTML = html;  \/\/ letak <table> terus dalam .table-container\n            document.getElementById(\"jumlahKempen\").textContent = `Jumlah Kempen: ${docs.length}`;\n\n            \/\/ \ud83c\udd95 Bind butang \u201cKira\u201d untuk pecahan Gagal\/Bukan WA\n            document.querySelectorAll('.btn-kira').forEach(btn => {\n                btn.addEventListener('click', async (e) => {\n                    const id = e.currentTarget.getAttribute('data-id');\n                    await kiraStatusTerperinci(id);\n                });\n            });\n\n            \/\/ tulis nota DI LUAR bekas scroll\n            const notaEl = document.getElementById('notaGagal');\n            if (notaEl) {\n                if (docs.some(d => (typeof d.data().jumlahBukanWhatsapp !== 'number'))) {\n                    notaEl.innerHTML = `* Angka \u201cGagal\u201d buat masa ini termasuk nombor Bukan WhatsApp. Tekan \u201c\ud83d\udd0d Kira\u201d untuk pecahan tepat.`;\n                } else {\n                    notaEl.innerHTML = ''; \/\/ kosongkan kalau semua sudah dikira\n                }\n            }\n\n            if (docs.length >= 20) {\n                Swal.fire({\n                    icon: \"warning\",\n                    title: \"\u26a0\ufe0f Terlalu Banyak Kempen\",\n                    text: \"Anda mempunyai lebih 20 kempen. Sila padam kempen lama untuk elakkan sistem lambat.\",\n                    confirmButtonText: \"Faham\",\n                    confirmButtonColor: \"#ff9800\"\n                });\n            }\n\n            \/\/ \u2705 Selepas semua loop selesai, baru update Firestore\n            for (const item of perluUpdate) {\n                await db.collection(\"campaigns\").doc(item.id).update({ jumlahReply: item.reply });\n            }\n        }\n\n        async function paparLeads(campaignId, nama) {\n\n            Swal.fire({\n                icon: 'info',\n                title: 'Leads reply',\n                html: `<p>Loading leads untuk kempen: ${campaignId}<\/p>`,\n                showConfirmButton: false\n            });\n\n            const kempenDoc = await db.collection(\"campaigns\").doc(campaignId).get();\n            if (!kempenDoc.exists) {\n                Swal.fire(\"\u274c Ralat\", \"Kempen tidak dijumpai.\", \"error\");\n                return;\n            }\n\n            const kempenData = kempenDoc.data();\n            const pengirim = kempenData.pengirim;\n            const kempenNumbers = Array.isArray(kempenData.numbers) ? kempenData.numbers\n                : (Array.isArray(kempenData.senarai) ? kempenData.senarai : []);\n            const kempenSet = new Set(kempenNumbers.map(normPhone)); \/\/ \ud83d\udd27 normalise dulu\n\n            \/\/ \u2705 robust\n            const ts2 = kempenData.timestamp;\n            const timestampKempen = ts2?.toDate ? ts2.toDate() : (typeof ts2 === \"string\" ? new Date(ts2) : new Date(0));\n\n\n            const snap = await db.collection(\"broadcast_leads\")\n                .where(\"source\", \"==\", pengirim)\n                .where(\"replyAt\", \">=\", timestampKempen)\n                .get();\n\n            const csvRows = [[\"No Telefon\", \"Mesej\", \"Waktu\"]];\n            const leadsList = [];\n            let table = `<div style=\"overflow-x:auto;\"><table><thead><tr><th>No Tel<\/th><th>Mesej<\/th><th>Waktu<\/th><\/tr><\/thead><tbody>`;\n\n            snap.forEach(doc => {\n                if (kempenSet.has(normPhone(doc.id))) {\n                    const data = doc.data();\n                    const when = data.replyAt?.toDate ? data.replyAt.toDate()\n                        : (typeof data.replyAt === \"string\" ? new Date(data.replyAt) : null);\n                    const whenStr = (when && !isNaN(when)) ? when.toLocaleString(\"ms-MY\") : \"-\";\n\n                    leadsList.push({ phone: doc.id, mesej: data.firstReply || \"-\", waktu: whenStr });\n                    csvRows.push([doc.id, data.firstReply || \"-\", whenStr]);\n                    table += `<tr><td>${doc.id}<\/td><td>${data.firstReply || '-'}<\/td><td>${whenStr}<\/td><\/tr>`;\n                }\n            });\n\n            table += `<\/tbody><\/table><\/div>`;\n\n            \/\/ \u2705 Kemaskini jumlahReply ke Firestore jika ada perubahan\n            const jumlahReplyFirestore = kempenData.jumlahReply || 0;\n            if (leadsList.length !== jumlahReplyFirestore) {\n                await db.collection(\"campaigns\").doc(campaignId).update({\n                    jumlahReply: leadsList.length\n                });\n            }\n\n            if (leadsList.length === 0) {\n                Swal.fire(\"\u274c Tiada Leads\", \"Tiada nombor yang membalas untuk kempen ini.\", \"info\");\n                return;\n            }\n\n            Swal.fire({\n                title: `Leads Reply \u2013 ${campaignId}`,\n                html: table,\n                width: '800px',\n                showDenyButton: true,\n                showCancelButton: true,\n                confirmButtonText: '\ud83d\udce4 Import ke Broadcast Biasa',\n                denyButtonText: '\ud83e\udd16 Import ke Broadcast AI',\n                cancelButtonText: '\ud83d\udce5 Muat Turun CSV',\n                didOpen: () => {\n                    const swalFooter = Swal.getActions();\n                    const downloadBtn = document.createElement(\"button\");\n                    downloadBtn.innerText = \"\ud83d\udce5 Muat Turun CSV\";\n                    downloadBtn.className = \"swal2-cancel swal2-styled\";\n                    downloadBtn.style.marginRight = \"10px\";\n                    downloadBtn.onclick = () => muatTurunCSV(csvRows);\n                    swalFooter.insertBefore(downloadBtn, swalFooter.firstChild);\n                }\n            }).then(result => {\n                if (result.isConfirmed || result.isDenied) {\n                    localStorage.setItem(\"leadsAutoBroadcast\", JSON.stringify({\n                        leads: leadsList,\n                        namaKempen: nama\n                    }));\n                    const url = result.isConfirmed\n                        ? \"https:\/\/abatablaster.xyz\/index.php\/whatsapp-blaster\/\"\n                        : \"https:\/\/abatablaster.xyz\/index.php\/ai-broadcast-generator\/\";\n                    Swal.fire(result.isConfirmed ? \"\ud83d\udce4 Siap!\" : \"\ud83e\udd16 Siap!\", \"Leads dimasukkan ke borang.\", \"success\")\n                        .then(() => window.location.href = url);\n                }\n            });\n        }\n\n        function muatTurunCSV(rows) {\n            const csvContent = rows.map(e => e.join(\",\")).join(\"\\n\");\n            const blob = new Blob([csvContent], { type: \"text\/csv;charset=utf-8;\" });\n            const url = URL.createObjectURL(blob);\n\n            const a = document.createElement(\"a\");\n            a.href = url;\n            a.download = \"leads_reply.csv\";\n            a.style.display = \"none\";\n            document.body.appendChild(a);\n            a.click();\n            document.body.removeChild(a);\n        }\n\n        async function padamKempen(id, nama) {\n            const confirm = await Swal.fire({\n                icon: \"warning\",\n                title: \"Padam Kempen\",\n                html: `Anda pasti ingin padam kempen <b>${nama}<\/b>? Tindakan ini tidak boleh dibatalkan.`,\n                showCancelButton: true,\n                confirmButtonText: \"Padam\",\n                cancelButtonText: \"Batal\",\n                confirmButtonColor: \"#e53935\"\n            });\n\n            \/\/ \u2705 TAMBAH sebelum delete\n            const check = await db.collection(\"campaigns\").doc(id).get();\n            if (!check.exists || check.data().uid !== currentUserUid) {\n                Swal.fire(\"\u274c Ralat\", \"Anda tiada kebenaran padam kempen ini.\", \"error\");\n                return;\n            }\n\n            if (confirm.isConfirmed) {\n                await db.collection(\"campaigns\").doc(id).delete();\n                Swal.fire(\"\u2705 Berjaya\", \"Kempen telah dipadam.\", \"success\");\n                const selected = document.getElementById(\"pilihNombor\").value;\n                if (selected) loadKempenUntukNombor(selected);\n            }\n        }\n\n        async function pilihPadamSemua() {\n            const { value: statusToDelete } = await Swal.fire({\n                title: \"Padam Semua Kempen\",\n                input: \"select\",\n                inputOptions: {\n                    MENUNGGU: \"MENUNGGU\",\n                    SEDANG_HANTAR: \"SEDANG_HANTAR\",\n                    SELESAI: \"SELESAI\",\n                    GAGAL: \"GAGAL\",\n                    DIBATALKAN: \"DIBATALKAN\"\n                },\n                inputPlaceholder: \"Pilih status kempen untuk dipadam\",\n                showCancelButton: true,\n                confirmButtonText: \"Padam Semua\",\n                confirmButtonColor: \"#d32f2f\"\n            });\n\n            if (!statusToDelete) return;\n\n            const selected = document.getElementById(\"pilihNombor\").value;\n            if (!selected) return;\n\n            \/\/ \u2705 tapis ikut pemilik\n            const qDel = await db.collection(\"campaigns\")\n                .where(\"uid\", \"==\", currentUserUid)\n                .where(\"pengirim\", \"==\", selected)\n                .where(\"status\", \"==\", statusToDelete)\n                .get();\n\n            let batch = db.batch();\n            let count = 0;\n            for (const d of qDel.docs) {\n                batch.delete(d.ref);\n                count++;\n                if (count % 500 === 0) {\n                    await batch.commit();\n                    batch = db.batch();\n                }\n            }\n            await batch.commit();\n\n            Swal.fire(\"\ud83d\uddd1\ufe0f Selesai\", `Semua kempen status ${statusToDelete} telah dipadam.`, \"success\");\n            loadKempenUntukNombor(selected);\n        }\n\n        \/\/ Kira semua status (paginate by 500)\n        async function _kiraStatusSenarai(db, campaignId) {\n            const base = db.collection('campaigns').doc(campaignId).collection('senaraiStatus');\n            const idField = firebase.firestore.FieldPath.documentId();\n            let query = base.orderBy(idField).limit(500);\n\n            let last = null, kiraGagal = 0, kiraBukanWA = 0, fetched = 0;\n\n            while (true) {\n                const snap = last ? await query.startAfter(last).get() : await query.get();\n                if (snap.empty) break;\n\n                snap.docs.forEach(d => {\n                    const s = String(d.data()?.status || '').toUpperCase();\n                    if (s === 'GAGAL') kiraGagal++;\n                    else if (s === 'BUKAN_WHATSAPP') kiraBukanWA++;\n                });\n\n                fetched += snap.size;\n                last = snap.docs[snap.docs.length - 1];\n                if (snap.size < 500) break;\n            }\n            return { gagal: kiraGagal, bukanWa: kiraBukanWA, totalDoc: fetched };\n        }\n\n        async function kiraStatusTerperinci(campaignId) {\n            try {\n                if (typeof Swal !== 'undefined') {\n                    Swal.fire({ icon: 'info', title: 'Mengira...', text: 'Sila tunggu seketika.', showConfirmButton: false });\n                }\n                const { gagal, bukanWa } = await _kiraStatusSenarai(db, campaignId);\n\n                \/\/ Simpan ke dokumen (cache)\n                await db.collection('campaigns').doc(campaignId).update({\n                    jumlahBukanWhatsapp: bukanWa,\n                    jumlahGagalMurni: gagal\n                });\n\n                \/\/ Update DOM baris yang berkenaan\n                const row = document.querySelector(`[data-rowid=\"${campaignId}\"]`);\n                if (row) {\n                    const colGagal = row.querySelector('.col-gagal');\n                    const colBW = row.querySelector('.col-bukanwa');\n                    if (colGagal) colGagal.textContent = gagal;     \/\/ GAGAL sebenar (murni)\n                    if (colBW) colBW.textContent = bukanWa;   \/\/ Bukan WhatsApp\n                }\n\n                if (typeof Swal !== 'undefined') {\n                    Swal.fire({ icon: 'success', title: 'Selesai', text: 'Kiraan telah dikemas kini.' });\n                }\n            } catch (e) {\n                console.error(e);\n                if (typeof Swal !== 'undefined') {\n                    Swal.fire('Ralat', e.message || 'Gagal mengira status.', 'error');\n                } else {\n                    alert('Gagal mengira status: ' + (e.message || e));\n                }\n            }\n        }\n\n    <\/script>\n<\/body>\n\n<\/html>\n","protected":false},"excerpt":{"rendered":"<p>Dashboard Broadcast \ud83d\udcca Dashboard Kempen Broadcast Pilih Nombor WhatsApp: Status: \u2013 Semua \u2013MENUNGGUSEDANG_HANTARSELESAIGAGALDIBATALKAN Tarikh (YYYY-MM-DD): \ud83d\uddd1\ufe0f Padam Kempen Mengikut Status<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-476","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/abatablaster.xyz\/index.php\/wp-json\/wp\/v2\/pages\/476","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abatablaster.xyz\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/abatablaster.xyz\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/abatablaster.xyz\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/abatablaster.xyz\/index.php\/wp-json\/wp\/v2\/comments?post=476"}],"version-history":[{"count":26,"href":"https:\/\/abatablaster.xyz\/index.php\/wp-json\/wp\/v2\/pages\/476\/revisions"}],"predecessor-version":[{"id":1068,"href":"https:\/\/abatablaster.xyz\/index.php\/wp-json\/wp\/v2\/pages\/476\/revisions\/1068"}],"wp:attachment":[{"href":"https:\/\/abatablaster.xyz\/index.php\/wp-json\/wp\/v2\/media?parent=476"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}