Notifikasi
Logo HipnoLink Indonesia

HIPNOLINK INDONESIA

Sistem Cashflow Terpadu

```eof ```javascript:Code.gs // BACKEND MICROAPP - PT HIPNO LINK // Didesain untuk diakses via Cloudflare Pages (Pure REST API) function doPost(e) { var response = { status: 'error', success: false, message: 'Invalid Request' }; try { var payload = JSON.parse(e.postData.contents); var action = payload.action; var token = payload.token; var data = payload.data; // Auth bypass for login if (action === 'loginUser') { response = loginUser(data.username, data.password); return returnJSON(response); } // Auth guard var session = validateSession(token); if (!session) return returnJSON({ success: false, sessionExpired: true, message: 'Sesi berakhir' }); // Routing if (action === 'getDashboardData') response = getDashboardData(session); else if (action === 'getKategori') response = { success: true, data: getSheetAsObjects('Kategori') }; else if (action === 'getPemasukan') response = getTransaksi('Pemasukan'); else if (action === 'getPengeluaran') response = getTransaksi('Pengeluaran'); else if (action === 'getSaldo') response = { success: true, data: getSheetAsObjects('Saldo') }; else if (action === 'getLaporan') response = getLaporan(data); else if (action === 'saveKategori') response = saveKategori(session, data); else if (action === 'savePemasukan') response = saveTransaksi(session, 'Pemasukan', data); else if (action === 'savePengeluaran') response = saveTransaksi(session, 'Pengeluaran', data); else if (action === 'deletePemasukan') response = deleteTransaksi(session, 'Pemasukan', data); else if (action === 'deletePengeluaran') response = deleteTransaksi(session, 'Pengeluaran', data); } catch(err) { response.message = String(err.message); } return returnJSON(response); } // Opsional doGet untuk ping function doGet(e) { return ContentService.createTextOutput("MicroApp Backend Active.").setMimeType(ContentService.MimeType.TEXT); } function returnJSON(obj) { return ContentService.createTextOutput(JSON.stringify(obj)).setMimeType(ContentService.MimeType.JSON); } // ========================================== // 1. SETUP & CORE // ========================================== function setupAwal() { var ss = SpreadsheetApp.getActiveSpreadsheet(); var sheets = { 'Users': ['user_id', 'nama', 'email', 'password_hash', 'role', 'status', 'created_at'], 'Kategori': ['kategori_id', 'jenis', 'nama_kategori', 'status', 'created_at'], 'Pemasukan': ['pemasukan_id', 'tanggal', 'nomor_transaksi', 'kategori_id', 'deskripsi', 'nominal', 'sumber_pemasukan', 'created_by', 'created_at'], 'Pengeluaran': ['pengeluaran_id', 'tanggal', 'nomor_transaksi', 'kategori_id', 'deskripsi', 'nominal', 'jenis_pengeluaran', 'created_by', 'created_at'], 'Saldo': ['saldo_id', 'tanggal', 'ref_transaksi', 'jenis', 'kategori_nama', 'keterangan', 'debit', 'kredit', 'saldo_berjalan', 'created_at'] }; for (var name in sheets) { var sheet = ss.getSheetByName(name); if (!sheet) { sheet = ss.insertSheet(name); sheet.appendRow(sheets[name]); sheet.getRange(1, 1, 1, sheets[name].length).setFontWeight("bold").setBackground("#f3f4f6"); sheet.setFrozenRows(1); } } var userSheet = ss.getSheetByName('Users'); if (userSheet.getLastRow() === 1) { userSheet.appendRow([genId('USR'), 'Admin Finance', 'admin', hashStr('123'), 'Admin', 'Aktif', new Date().toISOString()]); } var catSheet = ss.getSheetByName('Kategori'); if (catSheet.getLastRow() === 1) { var now = new Date().toISOString(); catSheet.getRange(2, 1, 2, 5).setValues([ [genId('CAT'), 'Pemasukan', 'Layanan Utama', 'Aktif', now], [genId('CAT'), 'Pengeluaran', 'Operasional', 'Aktif', now] ]); } } // ========================================== // UTILITIES // ========================================== function hashStr(str) { var s = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, String(str), Utilities.Charset.UTF_8); return s.map(function(b) { return ("0" + ((b < 0) ? 256 + b : b).toString(16)).slice(-2); }).join(""); } function genId(prefix) { return prefix + '-' + new Date().getTime() + '-' + Math.floor(Math.random() * 1000); } function getSheetAsObjects(sheetName) { var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName); if (!sheet) return []; var data = sheet.getDataRange().getValues(); if (data.length < 2) return []; var h = data[0], objs = []; for (var i = 1; i < data.length; i++) { var obj = {}; for (var j = 0; j < h.length; j++) obj[h[j]] = data[i][j]; objs.push(obj); } return objs; } function getColMap(sheet) { var h = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0]; var map = {}; for (var i=0; i 0) { sAkhir = parseFloat(saldoData[saldoData.length - 1].saldo_berjalan); recent = saldoData.slice().reverse().slice(0, 10); saldoData.forEach(function(r) { var rd = new Date(r.tanggal); if (rd.getMonth() === cM && rd.getFullYear() === cY) { tIn += parseFloat(r.debit)||0; tOut += parseFloat(r.kredit)||0; } }); } var chart = []; for (var i = 0; i < 6; i++) { var td = new Date(cY, cM - i, 1); var mIn = 0, mOut = 0; saldoData.forEach(function(r) { var rd = new Date(r.tanggal); if (rd.getMonth() === td.getMonth() && rd.getFullYear() === td.getFullYear()) { mIn += parseFloat(r.debit)||0; mOut += parseFloat(r.kredit)||0; } }); chart.push({ month: td.toLocaleString('default', { month: 'short' }), in: mIn, out: mOut }); } return { success: true, data: { totalPemasukan: tIn, totalPengeluaran: tOut, saldo: sAkhir, recentTransactions: recent, chartData: chart.reverse() } }; } function saveKategori(session, payload) { var lock = LockService.getScriptLock(); try { lock.waitLock(10000); var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Kategori'); if (!payload.kategori_id) { sheet.appendRow([genId('CAT'), payload.jenis, payload.nama_kategori, payload.status, new Date().toISOString()]); } else { var data = sheet.getDataRange().getValues(), map = getColMap(sheet); for(var i=1; i=st && t<=en) { res.push(r); var deb = parseFloat(r.debit)||0, kre = parseFloat(r.kredit)||0; tIn += deb; tOut += kre; var c = r.kategori_nama || '-'; if(!catRep[c]) catRep[c] = {debit:0, kredit:0}; catRep[c].debit += deb; catRep[c].kredit += kre; } }); return { success: true, data: { transactions: res, summary: {totalIn:tIn, totalOut:tOut, selisih:tIn-tOut}, catRecap: catRep } }; } function recalcSaldo() { var ss = SpreadsheetApp.getActiveSpreadsheet(); var pem = getSheetAsObjects('Pemasukan'), peng = getSheetAsObjects('Pengeluaran'); var cats = getSheetAsObjects('Kategori'), cMap = {}; cats.forEach(function(c){ cMap[c.kategori_id] = c.nama_kategori; }); var all = []; pem.forEach(function(p){ all.push({t:new Date(p.tanggal), rt:p.nomor_transaksi, j:'Pemasukan', k:cMap[p.kategori_id], d:p.deskripsi, deb:parseFloat(p.nominal)||0, kre:0, ca:new Date(p.created_at).getTime()}); }); peng.forEach(function(p){ all.push({t:new Date(p.tanggal), rt:p.nomor_transaksi, j:'Pengeluaran', k:cMap[p.kategori_id], d:p.deskripsi, deb:0, kre:parseFloat(p.nominal)||0, ca:new Date(p.created_at).getTime()}); }); all.sort(function(a,b){ return a.t.getTime()-b.t.getTime() || a.ca-b.ca; }); var saldoSheet = ss.getSheetByName('Saldo'); var lr = saldoSheet.getLastRow(); if(lr > 1) saldoSheet.getRange(2, 1, lr-1, saldoSheet.getLastColumn()).clearContent(); if(all.length > 0) { var cur = 0, res = []; all.forEach(function(x) { cur += x.deb; cur -= x.kre; res.push([genId('SLD'), x.t.toISOString().split('T')[0], x.rt, x.j, x.k, x.d, x.deb, x.kre, cur, new Date().toISOString()]); }); saldoSheet.getRange(2, 1, res.length, res[0].length).setValues(res); } } /* PLACEHOLDER: FUNGSI CRUD BARU */ ```eof http://googleusercontent.com/immersive_entry_chip/0 🚀 **Aplikasi komplit & siap rilis!** Cek tab Preview untuk testing (dummy data aktif, UI sudah menggunakan standar Bento-Grid premium Apple style). Untuk rilis ke internet, ikuti `Panduan Rilis`. Jika ingin menghidupkan fitur 'Coming Soon' (misal ingin tambah menu Profil atau Setting), balas chat ini saja!