Rp 395.000
2 Siswa (5 Sesi)Rp 240.000
1 Siswa LunasRp 155.000
1 Belum LunasDaftar Jadwal, Pembayaran & WA Orang Tua
Alya Putri Maharani
Matematika & Fisika (SMA Kelas 11)
Trigonometri Dasar
Hukum Newton & Dinamika
Latihan Soal PAS
Bagas Pratama
Bahasa Inggris Conversation (SMP)
Speaking & Daily Idioms
Storytelling Practice
Input Siswa & Multi-Jadwal Les Baru
Otomatis tersimpan ke aplikasi & disinkronkan ke Google Drive/Sheets
Koneksi Web App Google Sheets / Drive
Terhubung otomatis 2 arah (Simpan & Muat Riwayat)
Catatan Penting Agar Sinkronisasi 2 Arah Berjalan Mulus:
Aplikasi ini sudah menggunakan protokol text/plain;charset=utf-8 + kompatibilitas ganda sehingga dapat langsung mengirim dan menarik data dari URL Web App Anda. Pastikan di Google Sheets Anda terdapat tab bernama JadwalLes atau perbarui skrip Code.gs Anda melalui menu Modular di sebelah kanan.
Riwayat Penyimpanan & Sinkronisasi Drive
Mencatat setiap penyimpanan lokal & sinkronisasi ke Google Sheets
Cek Koneksi Drive
Menggunakan data lokal HP (Failed to fetch)
14 Nov 2026, 16.03.01Kode Dipisahkan per Menu & Siap Upload Cloudflare
Setiap menu memiliki file JavaScript tersendiri sehingga saat Anda ingin meng-update satu menu, file menu lain tidak terganggu.
(function() {
'use strict';
var MenuCloud = {
sedangSync: false,
initCloudUI: function() {
var inp = document.getElementById('input-gas-url');
if (inp) inp.value = window.DataStore.ambilUrlSheets();
this.renderRiwayatSync();
},
simpanUrlConfig: function() {
var inp = document.getElementById('input-gas-url');
var url = window.DataStore.simpanUrlSheets(inp ? inp.value : '');
if (inp) inp.value = url;
window.UI.tampilkanToast('URL Web App berhasil disimpan!', 'success');
this.tarikDataDrive(true);
},
resetUrlDefault: function() {
var url = window.DataStore.simpanUrlSheets(window.DataStore.DEFAULT_URL);
var inp = document.getElementById('input-gas-url');
if (inp) inp.value = url;
window.UI.tampilkanToast('Dikembalikan ke URL Web App Default Anda!', 'success');
this.tarikDataDrive(true);
},
setStatusHeader: function(teks, mode) {
var statusEl = document.getElementById('sync-status-text');
var badgeLive = document.getElementById('text-cloud-live');
var iconSpin = document.getElementById('icon-sync-spin');
if (statusEl) statusEl.textContent = teks;
if (mode === 'LOADING') {
if (badgeLive) badgeLive.textContent = 'Syncing...';
if (iconSpin) iconSpin.classList.add('fa-spin');
} else if (mode === 'OK') {
if (badgeLive) badgeLive.textContent = 'Tersinkron Drive';
if (iconSpin) iconSpin.classList.remove('fa-spin');
} else {
if (badgeLive) badgeLive.textContent = 'Mode Lokal';
if (iconSpin) iconSpin.classList.remove('fa-spin');
}
},
// Sinkronisasi KELUAR (PUSH) yang mendukung Code.gs skema Array maupun Object
kirimDataDrive: function(tampilkanAlert, aksiKeterangan) {
var self = this;
var url = window.DataStore.ambilUrlSheets();
var data = window.DataStore.ambilData();
var labelAksi = aksiKeterangan || 'Simpan perubahan ke Drive/Sheets';
this.setStatusHeader('Menyimpan ke Google Drive/Sheets...', 'LOADING');
// Payload hibrida: Jika Code.gs memakai Array (forEach) maupun Object (payload.data),
// kita kirimkan Array di langkah pertama atau Object yang kompatibel.
// Agar bekerja pada Code.gs versi koleksi (payload.forEach) DAN versi baru (payload.data):
// Kita kirimkan Array langsung yang setiap elemennya memiliki properti lengkap!
var payloadKompatibel = data.map(function(item) {
return {
id: item.id,
namaSiswa: item.namaSiswa,
noWa: item.noWa,
telepon: item.noWa,
mapel: item.mapel,
mataPelajaran: item.mapel,
hari: item.hari,
jam: item.jam,
tarif: item.tarif,
jumlahSesi: item.jumlahSesi,
totalBiaya: item.totalBiaya,
biaya: item.totalBiaya,
sudahDibayar: item.sudahDibayar,
sisaTagihan: item.sisaTagihan,
lunas: item.lunas,
statusBayar: item.lunas ? 'Lunas' : (item.sudahDibayar > 0 ? 'Dicicil (Rp ' + item.sudahDibayar + ')' : 'Belum Bayar'),
pertemuan: item.pertemuan,
pertemuanJson: JSON.stringify(item.pertemuan || [])
};
});
// Kirim menggunakan text/plain agar lolos CORS Preflight Google Apps Script
fetch(url, {
method: 'POST',
redirect: 'follow',
headers: { 'Content-Type': 'text/plain;charset=utf-8' },
body: JSON.stringify(payloadKompatibel)
})
.then(function(res) {
return res.text();
})
.then(function(text) {
var parsed = null;
try { parsed = JSON.parse(text); } catch (e) {}
// Jika Code.gs pengguna ternyata mengharapkan { action: 'SYNC_ALL', data: [...] }
if (parsed && parsed.status === 'error') {
return fetch(url, {
method: 'POST',
redirect: 'follow',
headers: { 'Content-Type': 'text/plain;charset=utf-8' },
body: JSON.stringify({ action: 'SYNC_ALL', data: payloadKompatibel })
});
}
return true;
})
.then(function() {
var jam = new Date().toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' });
self.setStatusHeader('Tersinkron ke Drive (' + data.length + ' siswa • ' + jam + ')', 'OK');
window.DataStore.catatRiwayatSync(labelAksi, 'SUKSES', 'Tersimpan ke Google Sheets (' + data.length + ' siswa)');
self.renderRiwayatSync();
if (tampilkanAlert) {
window.UI.tampilkanToast('Berhasil disimpan & disinkronkan ke Google Drive/Sheets!', 'success');
}
})
.catch(function() {
// Fallback no-cors jika browser memblokir redirect response dari script.googleusercontent.com
fetch(url, {
method: 'POST',
mode: 'no-cors',
headers: { 'Content-Type': 'text/plain;charset=utf-8' },
body: JSON.stringify(payloadKompatibel)
}).then(function() {
var jam = new Date().toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' });
self.setStatusHeader('Terkirim ke Drive (' + jam + ')', 'OK');
window.DataStore.catatRiwayatSync(labelAksi, 'SUKSES', 'Dikirim ke Web App Drive (' + data.length + ' siswa)');
self.renderRiwayatSync();
if (tampilkanAlert) {
window.UI.tampilkanToast('Data dikirim ke Google Sheets!', 'success');
}
}).catch(function() {
self.setStatusHeader('Tersimpan di HP (Drive Offline)', 'LOCAL');
window.DataStore.catatRiwayatSync(labelAksi, 'LOKAL', 'Tersimpan di memori HP (Menunggu koneksi)');
self.renderRiwayatSync();
});
});
},
// Sinkronisasi MASUK (PULL) dari Web App Google Sheets ke Aplikasi
tarikDataDrive: function(tampilkanToast) {
var self = this;
var url = window.DataStore.ambilUrlSheets();
this.setStatusHeader('Mengambil riwayat data dari Drive...', 'LOADING');
var urlNoCache = url + (url.indexOf('?') === -1 ? '?' : '&') + 't=' + Date.now();
fetch(urlNoCache, {
method: 'GET',
redirect: 'follow'
})
.then(function(res) {
return res.json();
})
.then(function(json) {
if (json && json.status === 'error') {
throw new Error(json.message || 'Error pada Script Sheets');
}
var rawList = Array.isArray(json)
? json
: (json && Array.isArray(json.data) ? json.data : null);
if (Array.isArray(rawList) && rawList.length > 0) {
var normList = rawList.map(function(r) {
return window.DataStore.normalisasiItem(r);
}).filter(Boolean);
window.DataStore.simpanData(normList);
window.App.muatUlangUI();
var jam = new Date().toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' });
self.setStatusHeader('Sinkron dari Drive (' + normList.length + ' siswa • ' + jam + ')', 'OK');
window.DataStore.catatRiwayatSync('Tarik Data dari Drive', 'SUKSES', 'Memuat ' + normList.length + ' data siswa dari Google Sheets');
self.renderRiwayatSync();
if (tampilkanToast) {
window.UI.tampilkanToast('Berhasil memuat ' + normList.length + ' siswa dari Drive!', 'success');
}
} else if (Array.isArray(rawList) && rawList.length === 0) {
// Jika di Drive masih kosong tetapi di HP ada data, otomatis push data HP ke Drive!
var dataLokal = window.DataStore.ambilData();
if (dataLokal.length > 0) {
self.kirimDataDrive(false, 'Inisialisasi Data Awal ke Drive');
} else {
self.setStatusHeader('Terhubung ke Drive (Data Kosong)', 'OK');
}
}
})
.catch(function(err) {
self.setStatusHeader('Aktif di Memori HP (Siap Sync)', 'LOCAL');
window.DataStore.catatRiwayatSync('Cek Koneksi Drive', 'LOKAL', 'Menggunakan data lokal HP (' + (err.message || 'Cek tab Sheet JadwalLes') + ')');
self.renderRiwayatSync();
if (tampilkanToast) {
window.UI.tampilkanToast('Menggunakan data lokal. Pastikan Sheet bernama JadwalLes sudah ada.', 'warning');
}
});
},
renderRiwayatSync: function() {
var list = window.DataStore.ambilRiwayatSync();
var badgeCount = document.getElementById('badge-riwayat-count');
if (badgeCount) badgeCount.textContent = list.length;
var container = document.getElementById('riwayat-sync-list');
if (!container) return;
if (list.length === 0) {
container.innerHTML = '<p class="text-xs text-slate-400 text-center py-6">Belum ada riwayat penyimpanan tercatat.</p>';
return;
}
container.innerHTML = list.map(function(log) {
var badge = log.status === 'SUKSES'
? '<span class="px-2 py-0.5 rounded-full text-[9px] font-extrabold bg-emerald-100 text-emerald-800">TERSIMPAN DI DRIVE</span>'
: '<span class="px-2 py-0.5 rounded-full text-[9px] font-extrabold bg-amber-100 text-amber-800">MEMORI LOKAL</span>';
return [
'<div class="p-2.5 rounded-xl bg-slate-50 border border-slate-200/80 flex items-start justify-between gap-2 text-xs">',
' <div class="min-w-0">',
' <p class="font-bold text-slate-800 truncate">' + window.UI.escapeHtml(log.aksi) + '</p>',
' <p class="text-[11px] text-slate-500 mt-0.5">' + window.UI.escapeHtml(log.keterangan) + '</p>',
' <span class="text-[10px] text-slate-400 block mt-0.5"><i class="fa-regular fa-clock mr-1"></i>' + window.UI.escapeHtml(log.waktu) + '</span>',
' </div>',
' <div class="shrink-0">' + badge + '</div>',
'</div>'
].join('\n');
}).join('\n');
},
bersihkanRiwayat: function() {
window.DataStore.hapusRiwayatSync();
this.renderRiwayatSync();
window.UI.tampilkanToast('Riwayat log sinkronisasi dibersihkan.', 'info');
}
};
window.MenuCloud = MenuCloud;
})();