Cara Membuat Dashboard Realtime dari Google Sheets (Angka Update Sendiri)
6 menit baca · Diperbarui 2026-07-15
Bos minta angka penjualan hari ini. Kamu buka Sheets, screenshot, kirim WA. Sejam kemudian angkanya sudah basi dan dia minta lagi. Besok pola yang sama, minggu depan juga. Screenshot itu bukan laporan, itu pekerjaan manual yang menyamar jadi laporan.
Yang kamu butuhkan bukan chart cantik di dalam Sheets. Bikin satu halaman web yang narik data dari sheet, nampilin KPI dan grafik, lalu update sendiri. Kamu kasih satu link, bos buka kapan aja, angkanya selalu segar. Nggak usah buka sheet mentah, dan kamu berhenti jadi jembatan.
Dashboard di dalam Sheets vs web app yang bisa dibagikan
Chart bawaan Sheets itu enak buat kamu sendiri. Buat orang lain? Bermasalah. Kamu harus kasih akses editor atau viewer ke seluruh file, artinya mereka bisa lihat semua tab, termasuk rumus dan kolom gaji yang harusnya nggak kelihatan. Mau sembunyikan? Ribet. Mau kontrol siapa lihat apa? Nggak bisa presisi.
Web app beda kelas. Kamu kirim data yang sudah kamu pilih dan agregasi ke sebuah halaman HTML. Sheet tetap privat, yang dibagikan cuma tampilannya. Kalau kamu belum pernah bikin web app dari Sheets sama sekali, baca dulu panduan dasar bikin web app Google Spreadsheet biar paham alur doGet dan deploy-nya, lalu balik ke sini buat bagian dashboard-nya.
Rancang dulu metrik yang mau dipantau
Jangan mulai dari kode. Mulai dari pertanyaan: angka apa yang bikin bos berhenti panik? Biasanya tiga sampai lima, nggak lebih. Dashboard yang penuh dua puluh angka itu sama nggak bergunanya dengan sheet mentah.
- Penjualan: total hari ini, jumlah transaksi, rata-rata per transaksi, tren per hari.
- Stok: SKU di bawah ambang minimum, total nilai stok, barang paling laku.
- Target tim: persentase pencapaian per orang, sisa hari kerja, siapa yang di bawah garis.
Tentukan juga bentuk kolom di sheet sebelum ngoding. Satu baris = satu transaksi, kolom tanggal betulan bertipe Date (bukan teks), nominal betulan angka. Kalau data masuknya dari HP lewat form, atur strukturnya sekalian pakai form input data ke Spreadsheet dari HP supaya sumbernya rapi dari awal. Sampah masuk, sampah keluar.
Ambil dan agregasi data pakai getValues batch
Ini bagian yang paling sering bikin dashboard lemot. Orang loop baris satu-satu pakai getRange di dalam for. Tiap panggilan getValue itu satu round-trip ke Sheets. Seribu baris = seribu panggilan = dashboard yang loading sepuluh detik. Jangan gitu.
Ambil semua sekaligus dengan satu getValues, lalu olah di memori pakai JavaScript biasa. Satu round-trip, sisanya cepat.
// Code.gs
function doGet() {
return HtmlService.createHtmlOutputFromFile('Index')
.setTitle('Dashboard Penjualan')
.addMetaTag('viewport', 'width=device-width, initial-scale=1');
}
function getDashboardData() {
const sheet = SpreadsheetApp
.openById('MASUKKAN_ID_SPREADSHEET')
.getSheetByName('Penjualan');
const lastRow = sheet.getLastRow();
if (lastRow < 2) {
return { total: 0, transaksi: 0, rataRata: 0, perHari: [] };
}
// Kolom A: tanggal, B: produk, C: nominal.
// Ambil sekali untuk semua baris data, bukan per sel.
const rows = sheet.getRange(2, 1, lastRow - 1, 3).getValues();
const tz = Session.getScriptTimeZone();
let total = 0;
const perHari = {};
for (const [tanggal, produk, nominal] of rows) {
const nilai = Number(nominal) || 0;
total += nilai;
const key = Utilities.formatDate(new Date(tanggal), tz, 'yyyy-MM-dd');
perHari[key] = (perHari[key] || 0) + nilai;
}
const seri = Object.keys(perHari)
.sort()
.slice(-14) // 14 hari terakhir saja
.map(function (k) { return { tanggal: k, nominal: perHari[k] }; });
return {
total: total,
transaksi: rows.length,
rataRata: Math.round(total / rows.length),
perHari: seri
};
}Tampilkan angka dan kartu KPI di HTML
Sisi frontend-nya HTML polos plus sedikit CSS. Panggil fungsi server lewat google.script.run, terima objek data, tembak ke DOM. Grafik di contoh ini dibikin dari div CSS supaya jalan tanpa library eksternal apa pun. Cukup buat tren harian, dan nggak ada risiko CDN gagal load.
<!-- Index.html -->
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
body { font-family: system-ui, sans-serif; margin: 0;
background: #0f172a; color: #e2e8f0; padding: 24px; }
.kpi-grid { display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 16px; }
.card { background: #1e293b; border-radius: 12px; padding: 20px; }
.card .label { font-size: 13px; color: #94a3b8; }
.card .value { font-size: 28px; font-weight: 700; margin-top: 6px; }
.chart { margin-top: 24px; background: #1e293b;
border-radius: 12px; padding: 20px; }
.bar-row { display: flex; align-items: center; gap: 12px; margin: 8px 0; }
.bar-row .tgl { width: 96px; font-size: 12px; color: #94a3b8; }
.bar { height: 20px; background: #38bdf8; border-radius: 4px; min-width: 2px; }
.updated { margin-top: 16px; font-size: 12px; color: #64748b; }
</style>
</head>
<body>
<div class="kpi-grid">
<div class="card"><div class="label">Total Penjualan</div>
<div class="value" id="total">-</div></div>
<div class="card"><div class="label">Jumlah Transaksi</div>
<div class="value" id="transaksi">-</div></div>
<div class="card"><div class="label">Rata-rata / Transaksi</div>
<div class="value" id="rata">-</div></div>
</div>
<div class="chart">
<div class="label">Penjualan per Hari</div>
<div id="grafik"></div>
</div>
<div class="updated" id="updated"></div>
<script>
function rupiah(n) {
return 'Rp ' + Number(n).toLocaleString('id-ID');
}
function render(data) {
document.getElementById('total').textContent = rupiah(data.total);
document.getElementById('transaksi').textContent = data.transaksi;
document.getElementById('rata').textContent = rupiah(data.rataRata);
var nilai = data.perHari.map(function (d) { return d.nominal; });
var maks = Math.max.apply(null, nilai.concat([1]));
document.getElementById('grafik').innerHTML = data.perHari.map(function (d) {
var lebar = Math.round((d.nominal / maks) * 100);
return '<div class="bar-row"><span class="tgl">' + d.tanggal + '</span>' +
'<div class="bar" style="width:' + lebar + '%"></div></div>';
}).join('');
document.getElementById('updated').textContent =
'Diperbarui ' + new Date().toLocaleTimeString('id-ID');
}
function muat() {
google.script.run
.withSuccessHandler(render)
.withFailureHandler(function (err) { console.error(err); })
.getDashboardData();
}
muat();
setInterval(muat, 30000); // auto-refresh tiap 30 detik
</script>
</body>
</html>Grafik: render chart dari data sheet
Bar CSS di atas sudah cukup buat 80% kebutuhan. Butuh yang lebih niat seperti garis tren atau donat kategori? Kamu bisa pakai Google Charts yang di-load dari gstatic di dalam HtmlService, atau tarik satu library chart ringan. Prinsipnya sama: server cuma kirim array data mentah, frontend yang menggambar. Jangan bikin server ngeluarin HTML grafik jadi, itu bikin logika susah dites dan susah diganti.
Auto-refresh: polling biar angka update sendiri
Rahasianya cuma dua baris di contoh tadi: panggil muat() sekali saat load, lalu setInterval tiap 30 detik. Halaman tetap terbuka di layar bos, tiap setengah menit dia narik data terbaru sendiri. Inilah yang bikin dashboard terasa hidup tanpa kamu sentuh apa pun.
Tapi hati-hati sama interval yang terlalu agresif. Refresh tiap 3 detik dikali sepuluh orang yang buka = banyak eksekusi, dan Apps Script punya batas. Angka 30-60 detik itu titik aman. Kalau kamu mau tahu batasnya di mana, baca batas kuota Google Apps Script sebelum nge-set interval terlalu rendah. Karena tiap polling tetap satu eksekusi, interval yang longgar itu rem utamanya; cache cuma bikin tiap eksekusi lebih murah.
Kontrol akses: link vs login
Waktu deploy, Apps Script tanya dua hal yang menentukan siapa boleh lihat. Jawablah sadar, jangan asal Next.
- Execute as: pilih 'Me' supaya script jalan pakai akses kamu ke sheet. Orang yang buka nggak butuh izin ke file mentah.
- Who has access: 'Anyone with the link' kalau dashboard publik dan datanya nggak sensitif. 'Anyone within [organisasi]' kalau mau dikunci ke akun kerja. 'Only myself' saat masih ngetes.
Kombinasi 'Execute as: Me' plus 'Anyone within organisasi' itu pas: sheet tetap privat total, tapi tim internal bisa buka dashboard-nya pakai login Google mereka. Butuh gerbang lebih ketat, sampai per orang? Di sini ada jebakan yang gampang kelewat. Session.getActiveUser().getEmail() cuma balik email kalau login pengunjung benar-benar terjadi. Pada deployment 'Anyone with the link', pengunjungnya anonim dan fungsi itu balik string kosong, bukan email siapa-siapa. Jadi gerbang per-email cuma andal saat login dipaksa, yaitu 'Execute as: Me' plus 'Anyone within [organisasi]' yang mengunci akses ke domain kerja kamu.
Konsekuensinya: perlakukan email kosong sebagai tolak, bukan lolos. Kalau kamu cuma cek 'email ada di daftar izin' tanpa menangani string kosong lebih dulu, satu salah setting deployment bisa bikin gerbang gagal diam-diam, atau malah nutup semua orang. Tulis fail-closed dari awal.
// Code.gs — gerbang per-email, fail-closed
var IZIN = ['bos@perusahaan.com', 'kamu@perusahaan.com'];
function getDashboardData() {
var email = Session.getActiveUser().getEmail();
// Email kosong = pengunjung anonim / di luar domain owner.
// Tolak duluan, jangan diteruskan ke pengecekan daftar.
if (!email || IZIN.indexOf(email) === -1) {
throw new Error('Akses ditolak');
}
// ... aman, lanjut ambil dan agregasi data seperti sebelumnya
}Deploy dan bagikan link
- Di editor Apps Script, klik Deploy > New deployment, pilih tipe Web app.
- Set Execute as dan Who has access sesuai bagian di atas, lalu Deploy.
- Salin URL web app yang keluar. Itu link yang kamu kirim ke bos atau klien.
- Tiap ubah kode, jangan lupa Deploy > Manage deployments > Edit > version New, biar perubahan naik ke URL yang sama.
Poin terakhir sering bikin orang bingung: kenapa kode sudah diubah tapi dashboard masih lama. Karena URL web app terkunci ke satu versi. Naikkan versi lewat Manage deployments, baru live. Kalau kamu masih baru banget di Apps Script, panduan Apps Script untuk pemula membahas siklus edit-deploy ini dari nol.
Rancang dulu, ngoding belakangan
Kode di atas jalan, tapi dashboard beneran punya detail yang gampang kelewat: metrik mana yang benar-benar dipantau, bentuk kolom sumber, aturan cache, siapa boleh lihat apa, kolom mana yang jangan pernah bocor. Kalau semua itu kamu putuskan sambil ngoding, hasilnya tambal sulam.
Lebih cepat kalau kamu tulis dulu spesifikasinya sebagai PRD, baru serahkan ke agent coding buat dibangun. Skematik bikin PRD tipe Web GAS lengkap dengan struktur sheet, daftar KPI, dan logika akses, jadi kamu tinggal eksekusi. Intip juga template siap pakai kalau mau titik mulai yang lebih dekat ke jadi.
Bangun dashboard Web GAS lewat PRDScreenshot basi tiap jam itu bukan takdir kerjaan kamu. Rancang dashboard-nya sekali dengan benar, dan biar link itu yang gantiin kamu jadi tukang lapor.