Login Pengelola: Akses kontrol penuh operasional & verifikasi rombongan.

Belum memiliki akun kemitraan biro perjalanan?

TRAVEL PARTNER PRONOJIWO
`; footer.innerHTML = ` `; } function saveManualCommission(isEdit = false) { const store = getStore(); const comIdInput = document.getElementById('mComId').value; const partnerId = document.getElementById('mComPartner').value; const trxId = document.getElementById('mComTrxId').value.trim() || 'MANUAL'; const dateVal = document.getElementById('mComDate').value || new Date().toISOString().split('T')[0]; const amountVal = Number(document.getElementById('mComAmount').value); const statusVal = document.getElementById('mComStatus').value; const methodVal = document.getElementById('mComMethod').value.trim() || '-'; const proofVal = document.getElementById('mComProof').value.trim() || '-'; const noteVal = document.getElementById('mComNote').value.trim() || '-'; if (isNaN(amountVal) || amountVal < 0) { showToast("Nominal komisi tidak boleh kosong atau minus!", "error"); return; } if (isEdit) { const item = store.commissions.find(c => c.id === comIdInput); if (item) { item.partnerId = partnerId; item.trxId = trxId; item.date = dateVal; item.amount = amountVal; item.status = statusVal; item.paidMethod = methodVal; item.proof = proofVal; item.note = noteVal; showToast(`Komisi ${item.id} berhasil diperbarui.`); } } else { const newId = "COM-" + Math.floor(100 + Math.random() * 900); store.commissions.unshift({ id: newId, trxId: trxId, partnerId: partnerId, amount: amountVal, status: statusVal, date: dateVal, paidMethod: methodVal, proof: proofVal, note: noteVal }); showToast(`Komisi baru ${newId} berhasil ditambahkan secara manual.`); } saveStore(store); closeModal(); navigate('commissions'); } function payCommission(comId) { const store = getStore(); const com = store.commissions.find(c => c.id === comId); if (!com) return; const proofNo = prompt("Masukkan No. Referensi Transfer Bank / Bukti Bayar:", "TRF-BCA-" + Math.floor(100000 + Math.random() * 900000)); if (proofNo) { com.status = "Dibayar"; com.paidMethod = "Transfer Bank"; com.proof = proofNo; saveStore(store); showToast(`Komisi ${com.id} berhasil ditandai LUNAS.`); navigate('commissions'); } } /* ========================================================================== 11. LOYALTY REWARDS & PENUKARAN (ADMIN VIEW) ========================================================================== */ function renderAdminRewards(container) { const store = getStore(); const pSet = store.pointSettings || DEFAULT_STORE.pointSettings; let totalPointsIn = 0; let totalPointsOut = 0; store.pointLedger.forEach(pl => { if (pl.type === 'IN') totalPointsIn += Number(pl.amount || 0); else totalPointsOut += Number(pl.amount || 0); }); const totalVerifiedSpend = store.transactions .filter(t => t.status === 'Terverifikasi') .reduce((sum, t) => sum + Number(t.total || 0), 0); const estAutoPoints = Math.floor(totalVerifiedSpend / Math.max(1000, Number(pSet.spendUnit || 100000))) * Number(pSet.pointsPerUnit || 1); container.innerHTML = `

Pronojiwo Partner Rewards & Konfigurasi Poin

Atur rasio konversi belanja ke poin secara manual dan kelola katalog hadiah kemitraan

Konversi Poin Manual Admin

Ketentuan Konversi Poin Belanja

Tentukan secara manual nilai belanja yang setara dengan perolehan poin mitra (Contoh: Total belanja Rp 100.000 = 1 Poin). Aturan ini dapat Anda sesuaikan kapan pun.

Rasio Saat Ini: Tiap ${formatRupiah(pSet.spendUnit)} = ${pSet.pointsPerUnit} Poin
Total Belanja Terverifikasi: ${formatRupiah(totalVerifiedSpend)}
Poin Otomatis Akumulatif: ${estAutoPoints.toLocaleString('id-ID')} Poin
Atur Nominal Konversi Manual
Poin
Arus Poin Loyalitas (Poin Masuk dari Belanja vs Poin Keluar untuk Hadiah)

Ledger transaksional poin kemitraan mitra aktif

Ledger
${store.rewards.map(r => { const reqSpend = r.reqSpendTarget || (r.points * (pSet.spendUnit / (pSet.pointsPerUnit || 1))); return `
${r.category} Stok: ${r.stock} Unit

${r.name}

${r.desc}

Kebutuhan Poin: ${r.points} Poin
Estimasi Belanja: ${formatRupiah(reqSpend)}
ID: ${r.id}
`;}).join('')}
`; setTimeout(() => { initChart('chartPointsFlow', { type: 'bar', data: { labels: ['Poin Diterbitkan (Masuk)', 'Poin Ditukarkan (Keluar)', 'Saldo Poin Beredar'], datasets: [{ label: 'Jumlah Poin', data: [totalPointsIn, totalPointsOut, Math.max(0, totalPointsIn - totalPointsOut)], backgroundColor: ['#10B981', '#E07A5F', '#0F3D3E'], borderRadius: 6 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true }, x: { grid: { display: false } } } } }); }, 50); } function handleSavePointSettings(e) { e.preventDefault(); const store = getStore(); const spendUnit = Number(document.getElementById('cfgSpendUnit').value); const pointsPerUnit = Number(document.getElementById('cfgPointsPerUnit').value); const minSpend = Number(document.getElementById('cfgMinSpend').value); if (spendUnit <= 0 || pointsPerUnit <= 0) { showToast("Nilai belanja dan poin harus lebih besar dari 0!", "error"); return; } store.pointSettings = { spendUnit: spendUnit, pointsPerUnit: pointsPerUnit, minSpendToEarn: minSpend, active: true }; saveStore(store); showToast(`Kebijakan poin disimpan: Tiap ${formatRupiah(spendUnit)} = ${pointsPerUnit} Poin.`); navigate('rewards'); } function recalculateAllPartnerPoints() { if (!confirm("Hitung ulang dan sinkronkan poin otomatis seluruh mitra berdasarkan riwayat belanja terverifikasi dan aturan rasio terbaru?")) return; const store = getStore(); const pSet = store.pointSettings || DEFAULT_STORE.pointSettings; const spendUnit = Math.max(1000, Number(pSet.spendUnit || 100000)); const ptsPerUnit = Number(pSet.pointsPerUnit || 1); const minSpend = Number(pSet.minSpendToEarn || 0); let totalUpdatedMitra = 0; store.partners.forEach(partner => { const partnerTrx = store.transactions.filter(t => t.partnerId === partner.id && t.status === 'Terverifikasi'); const totalSpend = partnerTrx.reduce((sum, t) => sum + Number(t.total || 0), 0); let earnedPointsFromSpend = 0; if (totalSpend >= minSpend) { earnedPointsFromSpend = Math.floor(totalSpend / spendUnit) * ptsPerUnit; } const usedClaims = store.redemptions .filter(r => r.partnerId === partner.id && r.status === 'Selesai') .reduce((sum, r) => sum + Number(r.points || 0), 0); const newBalance = Math.max(0, earnedPointsFromSpend - usedClaims); partner.balancePoints = newBalance; totalUpdatedMitra++; store.pointLedger.push({ id: "PL-" + Date.now().toString().slice(-4), partnerId: partner.id, date: new Date().toISOString().split('T')[0], type: "IN", amount: earnedPointsFromSpend, ref: "AUDIT-AUTO", desc: `Kalkulasi otomatis belanja total ${formatRupiah(totalSpend)} (Aturan: ${formatRupiah(spendUnit)} = ${ptsPerUnit} Poin)` }); }); saveStore(store); showToast(`Berhasil! Poin otomatis ${totalUpdatedMitra} mitra berhasil disinkronkan sesuai rasio terbaru.`); navigate('rewards'); } function deleteReward(rewardId) { if (!confirm("Hapus hadiah ini dari katalog?")) return; const store = getStore(); store.rewards = store.rewards.filter(r => r.id !== rewardId); saveStore(store); showToast('Hadiah berhasil dihapus dari katalog.'); navigate('rewards'); } function renderAdminRedemptions(container) { const store = getStore(); container.innerHTML = `

Pengajuan Penukaran Poin Hadiah

Persetujuan pencairan reward, pengiriman merchandise & cash transfer

${store.redemptions.map(red => { const partner = store.partners.find(p => p.id === red.partnerId) || { name: 'Mitra', wa: '0812' }; return ` `; }).join('')}
No. Klaim Mitra Pemohon Hadiah Ditukar Biaya Poin Status Aksi Persetujuan
${red.id}
${red.reqDate}
${partner.name}
${partner.wa}
${red.rewardName}
${red.note}
${red.points} Poin ${red.status} ${red.status !== 'Selesai' ? ` ` : ` Selesai Diserahkan `}
`; } function approveRedemption(redId) { const store = getStore(); const red = store.redemptions.find(r => r.id === redId); if (!red) return; const partner = store.partners.find(p => p.id === red.partnerId); const reward = store.rewards.find(r => r.id === redId); if (partner && partner.balancePoints < red.points) { showToast("Gagal: Saldo poin mitra tidak mencukupi!", "error"); return; } if (reward) { if (reward.stock <= 0) { showToast("Gagal: Stok reward telah habis!", "error"); return; } reward.stock -= 1; } if (partner) { partner.balancePoints -= red.points; } red.status = "Selesai"; store.pointLedger.push({ id: "PL-" + Date.now().toString().slice(-4), partnerId: red.partnerId, date: new Date().toISOString().split('T')[0], type: "OUT", amount: red.points, ref: red.id, desc: `Klaim reward ${red.rewardName} disetujui` }); saveStore(store); showToast(`Klaim reward ${red.id} disetujui! Poin terpotong dan stok berkurang.`); navigate('redemptions'); } /* ========================================================================== 12. LAPORAN & PENGATURAN (ADMIN VIEW) ========================================================================== */ function renderAdminReports(container) { const store = getStore(); container.innerHTML = `

Laporan & Ekspor Data Kemitraan

Unduh data CSV untuk pembukuan Excel atau laporan pemilik

Ringkasan Kinerja Kemitraan Bulan Ini

Total Reservasi Terlaksana: ${store.reservations.filter(r => r.status === 'Selesai').length} Bus
Total Belanja Oleh-Oleh: ${formatRupiah(store.transactions.reduce((a,b)=>a+b.total,0))}
Total Komisi Diberikan: ${formatRupiah(store.commissions.reduce((a,b)=>a+b.amount,0))}
Poin Loyalitas Beredar: ${store.partners.reduce((a,b)=>a+b.balancePoints,0)} Poin

Manajemen Cadangan & Data Lokal

Atur ulang data operasional kembali ke setelan awal.

Zona Bahaya: Reset Data

Tindakan ini akan mengembalikan seluruh data mitra, reservasi, dan transaksi ke setelan awal.

`; } function exportCSV(type) { const store = getStore(); let csvContent = "data:text/csv;charset=utf-8,"; if (type === 'reservations') { csvContent += "ID,Mitra,Rombongan,Tanggal,Jam,Wisatawan,Kendaraan,Status\n"; store.reservations.forEach(r => { csvContent += `"${r.id}","${r.partnerName}","${r.groupName}","${r.visitDate}","${r.visitTime}","${r.tourists}","${r.vehicleTypes}","${r.status}"\n`; }); } else { csvContent += "ID,MitraID,Tanggal,Kategori,Total,Komisi,Status\n"; store.transactions.forEach(t => { csvContent += `"${t.id}","${t.partnerId}","${t.date}","${t.category}","${t.total}","${t.commCalculated}","${t.status}"\n`; }); } const encodedUri = encodeURI(csvContent); const link = document.createElement("a"); link.setAttribute("href", encodedUri); link.setAttribute("download", `Pronojiwo_${type}_${Date.now()}.csv`); document.body.appendChild(link); link.click(); document.body.removeChild(link); showToast(`Data ${type} berhasil diekspor ke CSV!`); } function resetDemoData() { if (!confirm("Apakah Anda yakin ingin mereset seluruh data kembali ke setelan awal?")) return; localStorage.removeItem(STORAGE_KEY); localStorage.removeItem(SESSION_KEY); showToast("Data sistem telah direset!"); setTimeout(() => location.reload(), 600); } function renderAdminSettings(container) { container.innerHTML = `

Informasi Usaha & Kontak Pengelola

`; } /* ========================================================================== 13. MITRA PORTAL (MOBILE-FRIENDLY VIEWS) ========================================================================== */ function renderPartnerDashboard(container) { const store = getStore(); const currentPartnerId = AUTH_USER.partnerId || 'PTR-001'; const partner = store.partners.find(p => p.id === currentPartnerId) || store.partners[0]; const myReservations = store.reservations.filter(r => r.partnerId === partner.id); container.innerHTML = `
Mitra Resmi Wisata ${partner.id}

${partner.name}

PIC: ${partner.pic} • ${partner.city}

Saldo Poin Rewards

${partner.balancePoints} Poin

Total Komisi Diterima

${formatRupiah(partner.totalComm)}

Grafik Statistik Rombongan Anda

Riwayat reservasi dan estimasi pax rombongan wisata Anda

Live
WA Pengelola

Reservasi Rombongan Anda

${myReservations.length === 0 ? '

Belum ada reservasi aktif.

' : myReservations.slice(0, 3).map(res => `
${res.groupName}
${formatDate(res.visitDate)} • ${res.visitTime} WIB (${res.tourists} Pax)
${res.status}
`).join('')}
`; setTimeout(() => { const labels = myReservations.map(r => r.groupName.slice(0, 14)); const dataPax = myReservations.map(r => r.tourists); initChart('chartPartnerActivity', { type: 'bar', data: { labels: labels.length > 0 ? labels : ['Belum Ada Data'], datasets: [{ label: 'Pax Wisatawan', data: dataPax.length > 0 ? dataPax : [0], backgroundColor: '#0F3D3E', borderRadius: 6 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true }, x: { grid: { display: false } } } } }); }, 50); } function renderPartnerNewReservation(container) { const store = getStore(); const currentPartnerId = AUTH_USER.partnerId || 'PTR-001'; const partner = store.partners.find(p => p.id === currentPartnerId) || store.partners[0]; container.innerHTML = `

Formulir Reservasi Rombongan Wisata

Ajukan kedatangan rombongan ke pusat oleh-oleh Pronojiwo

`; } function handlePartnerSubmitReservation(e) { e.preventDefault(); const store = getStore(); const currentPartnerId = AUTH_USER.partnerId || 'PTR-001'; const partner = store.partners.find(p => p.id === currentPartnerId) || store.partners[0]; const newRes = { id: "RES-2026-" + Math.floor(100 + Math.random() * 900), partnerId: partner.id, partnerName: partner.name, groupName: document.getElementById('resGroupName').value, pic: document.getElementById('resPic').value, picWa: document.getElementById('resPicWa').value, visitDate: document.getElementById('resDate').value, visitTime: document.getElementById('resTime').value, estArrival: document.getElementById('resTime').value + " WIB", tourists: Number(document.getElementById('resTourists').value), vehicles: Number(document.getElementById('resVehicles').value), vehicleTypes: document.getElementById('resVehicleTypes').value, plateNumbers: document.getElementById('resPlateNumbers').value || "-", guides: 1, drivers: 1, leaders: 1, destination: document.getElementById('resDestination').value, status: "Menunggu", actualVisitors: 0, note: document.getElementById('resNotes').value || "-" }; store.reservations.unshift(newRes); saveStore(store); showToast("Reservasi berhasil diajukan! Menunggu konfirmasi admin."); navigate('partnerReservations'); } function renderPartnerReservations(container) { const store = getStore(); const currentPartnerId = AUTH_USER.partnerId || 'PTR-001'; const myReservations = store.reservations.filter(r => r.partnerId === currentPartnerId); container.innerHTML = `

Riwayat Reservasi Rombongan

${myReservations.length === 0 ? '
Belum ada data reservasi.
' : myReservations.map(res => `
${res.id}

${res.groupName}

${res.status}
Jadwal: ${formatDate(res.visitDate)} (${res.visitTime} WIB)
Peserta: ${res.tourists} Pax (${res.vehicles} Unit)
Kendaraan: ${res.vehicleTypes}
Fasilitas: ${res.destination}
`).join('')}
`; } function renderPartnerAgreement(container) { const store = getStore(); const currentPartnerId = AUTH_USER.partnerId || 'PTR-001'; const agr = store.agreements.find(a => a.partnerId === currentPartnerId) || store.agreements[0]; container.innerHTML = `
Perjanjian Kemitraan

${agr.id}

${agr.status}
Model Komisi Yang Anda Dapatkan:
${agr.model === 'PERCENT' ? agr.val + '% dari Total Transaksi' : agr.model === 'NOMINAL' ? formatRupiah(agr.nominal) + ' / Rombongan' : agr.val + '% + ' + formatRupiah(agr.nominal)}

Dasar Perhitungan: ${agr.baseType}

Syarat & Ketentuan Kemitraan:

${agr.terms}

* Perjanjian ini dilindungi sistem snapshot aturan transaksi dan berlaku hingga ${formatDate(agr.endDate)}.
`; } function renderPartnerTransactions(container) { const store = getStore(); const currentPartnerId = AUTH_USER.partnerId || 'PTR-001'; const myTrx = store.transactions.filter(t => t.partnerId === currentPartnerId); container.innerHTML = `

Riwayat Belanja Rombongan Anda

${myTrx.map(t => ` `).join('')}
Tanggal & Ref Rombongan Total Belanja Hak Komisi Status
${t.id}
${formatDate(t.date)}
${t.groupName} ${formatRupiah(t.total)} ${formatRupiah(t.commCalculated)} ${t.status}
`; } function renderPartnerCommissions(container) { const store = getStore(); const currentPartnerId = AUTH_USER.partnerId || 'PTR-001'; const myCommissions = store.commissions.filter(c => c.partnerId === currentPartnerId); container.innerHTML = `

Status Pembayaran Komisi Anda

${myCommissions.map(c => `
${formatRupiah(c.amount)}
Ref Transaksi: ${c.trxId} • ${formatDate(c.date)}
${c.status}
${c.proof !== '-' ? c.proof : 'Menunggu transfer kasir'}
`).join('')}
`; } function renderPartnerPoints(container) { const store = getStore(); const currentPartnerId = AUTH_USER.partnerId || 'PTR-001'; const partner = store.partners.find(p => p.id === currentPartnerId) || store.partners[0]; container.innerHTML = `
Saldo Poin Anda

${partner.balancePoints} Poin

Dapatkan terus poin dari setiap kedatangan rombongan wisata.

Katalog Penukaran Hadiah

${store.rewards.map(r => `
${r.category}

${r.name}

${r.desc}

Biaya: ${r.points} Poin
`).join('')}
`; } function requestRedeem(rewardId) { const store = getStore(); const currentPartnerId = AUTH_USER.partnerId || 'PTR-001'; const partner = store.partners.find(p => p.id === currentPartnerId); const reward = store.rewards.find(r => r.id === rewardId); if (!partner || !reward) return; if (partner.balancePoints < reward.points) { showToast("Poin Anda tidak mencukupi untuk hadiah ini.", "error"); return; } const note = prompt(`Konfirmasi penukaran ${reward.name} (${reward.points} Poin)\nMasukkan catatan (misal: Rekening transfer / Alamat kirim / Ukuran kaos):`, "Rekening BCA 1234567890 an Danang"); if (!note) return; const newClaim = { id: "RED-" + Math.floor(100 + Math.random() * 900), partnerId: partner.id, rewardId: reward.id, rewardName: reward.name, points: reward.points, reqDate: new Date().toISOString().split('T')[0], status: "Menunggu Persetujuan", note: note }; store.redemptions.unshift(newClaim); saveStore(store); showToast("Pengajuan penukaran poin berhasil dikirim ke admin!"); navigate('partnerRedemptions'); } function renderPartnerRedemptions(container) { const store = getStore(); const currentPartnerId = AUTH_USER.partnerId || 'PTR-001'; const myClaims = store.redemptions.filter(r => r.partnerId === currentPartnerId); container.innerHTML = `

Riwayat Klaim Hadiah Anda

${myClaims.map(c => `
${c.rewardName}
${c.points} Poin • ${formatDate(c.reqDate)}
${c.note}
${c.status}
`).join('')}
`; } /* ========================================================================== 14. PUBLIC FORMS ========================================================================== */ function renderPublicRegister(container) { container.innerHTML = `
Kemitraan Biro Wisata

Daftar Jadi Mitra Wisata Pronojiwo

Daftarkan biro perjalanan Anda dan nikmati fasilitas komisi menarik, poin loyalitas, serta kemudahan transit rombongan bus.

Butuh bantuan cepat? Hubungi WA Pengelola (085739060626)
`; } function handlePublicRegisterSubmit(e) { e.preventDefault(); const store = getStore(); const newApp = { id: "APP-" + Math.floor(100 + Math.random() * 900), company: document.getElementById('regCompany').value, pic: document.getElementById('regPic').value, wa: document.getElementById('regWa').value, email: document.getElementById('regEmail').value, city: document.getElementById('regCity').value, route: document.getElementById('regRoute').value, services: "Biro Wisata", estTrips: document.getElementById('regTrips').value || "2-4 kali", status: "Menunggu Verifikasi", date: new Date().toISOString().replace('T', ' ').slice(0, 16) }; store.partnerApplications.unshift(newApp); saveStore(store); alert(`Pendaftaran berhasil dikirim dengan ID: ${newApp.id}! Tim kami akan segera memverifikasi dan menghubungi WhatsApp Anda.`); navigate('dashboard'); } function renderPublicReservation(container) { renderPartnerNewReservation(container); } /* ========================================================================== 15. MODAL FORM BUILDERS ========================================================================== */ function renderReservationModal() { const store = getStore(); const title = document.getElementById('modalTitle'); const body = document.getElementById('modalBody'); const footer = document.getElementById('modalFooter'); title.innerText = "Catat Reservasi Rombongan"; body.innerHTML = `
`; footer.innerHTML = ` `; } function saveAdminReservation() { const store = getStore(); const partnerId = document.getElementById('mResPartner').value; const partner = store.partners.find(p => p.id === partnerId); const newRes = { id: "RES-2026-" + Math.floor(100 + Math.random() * 900), partnerId: partner.id, partnerName: partner.name, groupName: document.getElementById('mResGroup').value, pic: partner.pic, picWa: partner.wa, visitDate: document.getElementById('mResDate').value, visitTime: document.getElementById('mResTime').value, estArrival: document.getElementById('mResTime').value + " WIB", tourists: Number(document.getElementById('mResPax').value), vehicles: 1, vehicleTypes: "Bus Pariwisata", plateNumbers: "N 1234 XX", guides: 1, drivers: 1, leaders: 1, destination: "Pusat Oleh-Oleh", status: "Dikonfirmasi", actualVisitors: Number(document.getElementById('mResPax').value), note: "Diinput oleh admin" }; store.reservations.unshift(newRes); saveStore(store); closeModal(); showToast("Reservasi rombongan berhasil disimpan!"); navigate('reservations'); } function renderTransactionModal() { const store = getStore(); const title = document.getElementById('modalTitle'); const body = document.getElementById('modalBody'); const footer = document.getElementById('modalFooter'); title.innerText = "Catat Transaksi Belanja Oleh-Oleh"; body.innerHTML = `
`; footer.innerHTML = ` `; } function saveAdminTrx() { const store = getStore(); const resId = document.getElementById('mTrxRes').value; const res = store.reservations.find(r => r.id === resId); const total = Number(document.getElementById('mTrxTotal').value); const newTrx = { id: "TRX-" + Math.floor(1000 + Math.random() * 9000), resId: res.id, partnerId: res.partnerId, date: new Date().toISOString().split('T')[0], groupName: res.groupName, category: document.getElementById('mTrxCat').value, items: document.getElementById('mTrxItems').value || "Produk Pilihan", qty: 1, unitPrice: total, discount: 0, total: total, status: "Draft", commCalculated: 0, note: "Input Kasir" }; store.transactions.unshift(newTrx); saveStore(store); closeModal(); showToast("Transaksi belanja disimpan sebagai Draft. Silakan verifikasi untuk mencairkan komisi!"); navigate('transactions'); } function renderRewardModal() { const store = getStore(); const pSet = store.pointSettings || DEFAULT_STORE.pointSettings; const title = document.getElementById('modalTitle'); const body = document.getElementById('modalBody'); const footer = document.getElementById('modalFooter'); title.innerText = "Tambah Hadiah Baru & Hitung Konversi Poin"; body.innerHTML = `
Kalkulator Konversi Belanja ke Poin Manual Admin

Tentukan nominal belanja yang dibutuhkan atau masukkan jumlah poin secara langsung. Anda juga dapat mengubah ketentuan rasio konversi sewaktu-waktu.

* Rasio aktif: Tiap Rp ${Number(pSet.spendUnit).toLocaleString('id-ID')} belanja setara ${pSet.pointsPerUnit} poin.

`; footer.innerHTML = ` `; } function calcRewardPointsFromTargetSpend() { const spendRate = Number(document.getElementById('mRwdSpendRate').value) || 100000; const pointsRate = Number(document.getElementById('mRwdPointsRate').value) || 1; const targetSpend = Number(document.getElementById('mRwdTargetSpend').value) || 0; if (spendRate > 0 && pointsRate > 0) { const points = Math.max(1, Math.round((targetSpend / spendRate) * pointsRate)); document.getElementById('mRwdPoints').value = targetSpend > 0 ? points : ''; updateRewardCalcHint(spendRate, pointsRate); } } function calcRewardTargetSpendFromPoints() { const spendRate = Number(document.getElementById('mRwdSpendRate').value) || 100000; const pointsRate = Number(document.getElementById('mRwdPointsRate').value) || 1; const points = Number(document.getElementById('mRwdPoints').value) || 0; if (spendRate > 0 && pointsRate > 0) { const targetSpend = Math.round((points / pointsRate) * spendRate); document.getElementById('mRwdTargetSpend').value = points > 0 ? targetSpend : ''; updateRewardCalcHint(spendRate, pointsRate); } } function calcRewardPointsFromSpend() { const targetSpend = Number(document.getElementById('mRwdTargetSpend').value) || 0; if (targetSpend > 0) { calcRewardPointsFromTargetSpend(); } else { const points = Number(document.getElementById('mRwdPoints').value) || 0; if (points > 0) calcRewardTargetSpendFromPoints(); } const spendRate = Number(document.getElementById('mRwdSpendRate').value) || 100000; const pointsRate = Number(document.getElementById('mRwdPointsRate').value) || 1; updateRewardCalcHint(spendRate, pointsRate); } function updateRewardCalcHint(spendRate, pointsRate) { const hint = document.getElementById('mRwdHint'); if (hint) { hint.innerText = `* Rasio aktif: Tiap Rp ${spendRate.toLocaleString('id-ID')} belanja setara ${pointsRate} poin.`; } } function saveRewardModal() { const store = getStore(); const name = document.getElementById('mRwdName').value.trim(); const points = Number(document.getElementById('mRwdPoints').value); const stock = Number(document.getElementById('mRwdStock').value); const targetSpend = Number(document.getElementById('mRwdTargetSpend').value) || (points * (Number(document.getElementById('mRwdSpendRate').value || 100000) / Number(document.getElementById('mRwdPointsRate').value || 1))); if (!name) { showToast("Nama hadiah wajib diisi!", "error"); return; } if (isNaN(points) || points <= 0) { showToast("Biaya poin hadiah harus lebih besar dari 0!", "error"); return; } if (isNaN(stock) || stock < 0) { showToast("Stok hadiah tidak boleh minus!", "error"); return; } const newRwd = { id: "RWD-" + Math.floor(10 + Math.random() * 90), name: name, category: document.getElementById('mRwdCat').value, points: points, stock: stock, active: document.getElementById('mRwdActive').value === 'true', desc: document.getElementById('mRwdDesc').value.trim() || "-", reqSpendTarget: targetSpend }; store.rewards.push(newRwd); saveStore(store); closeModal(); showToast(`Hadiah baru "${newRwd.name}" berhasil ditambahkan (${points} Poin)!`); navigate('rewards'); } /* PLACEHOLDER: JS BARU */ const ACTIVE_CHARTS = {}; function initChart(canvasId, config) { try { const canvas = document.getElementById(canvasId); if (!canvas) return; if (ACTIVE_CHARTS[canvasId]) { ACTIVE_CHARTS[canvasId].destroy(); } ACTIVE_CHARTS[canvasId] = new Chart(canvas, config); } catch (err) { console.warn("Gagal inisialisasi grafik:", canvasId, err); } } window.onload = function() { initApp(); };