Guru Les Pro v4

Mode Lokal

Sinkronisasi 2 Arah Drive, WA Jadwal & Cicilan

Aktif di Memori HP (Siap Sync)
ARSITEKTUR MODULAR v4

Kode 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.

js/menu-cloud.js Siap Salin / Update
(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;
    })();