Pembaruan Template · Juli 2026
Kami memperbaiki bug pencatatan Reksadana dan merapikan template. Agar perbaikan aktif di spreadsheet Anda, perbarui Apps Script sekali saja — ikuti 4 langkah di bawah.
Tenang, data Anda tidak akan hilang. Langkah ini hanya memperbarui kode di balik layar — seluruh transaksi, aset, dan pengaturan Anda tetap utuh.
📋 Kode Apps Script Terbaru
Klik Salin Kode, lalu tempel di Langkah 2. Sudah termasuk perbaikan bug Reksadana.
/**
* =========================================================================
* DompetKu - Google Apps Script Backend (Full CRUD, Multi-Tab)
* =========================================================================
*
* Mendukung 6 Tab Spreadsheet:
* Transactions, Accounts, Assets, BudgetCategories, Debts, Settings
*
* CARA DEPLOY / UPDATE:
* 1. Buka Google Spreadsheet Database Anda.
* 2. Klik Extensions > Apps Script.
* 3. Hapus SEMUA kode lama, lalu paste kode ini.
* 4. Klik Deploy > Manage deployments > Edit (ikon pensil).
* 5. Ubah Version ke "New version", klik Deploy.
* 6. Jika deploy baru: Deploy > New deployment > Web app > Execute as: Me, Access: Anyone.
*/
// ===================== KONFIGURASI =====================
const VALID_SHEETS = ['Transactions', 'Accounts', 'Fixed Income Investment', 'BudgetCategories', 'Debts', 'Settings', 'AssetsNonLiquid', 'Saham', 'Crypto', 'Reksadana'];
// Header kolom untuk setiap tab (urutan HARUS sama dengan di Spreadsheet)
const HEADERS = {
Transactions: ['id', 'date', 'desc', 'location', 'amount', 'category', 'icon', 'status', 'account', 'type'],
Accounts: ['id', 'name', 'type', 'balance', 'currency', 'icon', 'startDate', 'valuationReminder', 'lastValuationUpdate'],
'Fixed Income Investment': ['id', 'title', 'category', 'purchasePrice', 'currentValue', 'purchaseDate', 'location', 'icon', 'equity', 'notes', 'ticker', 'shares', 'avgCost', 'interestRate', 'maturityDate', 'subType'],
BudgetCategories: ['id', 'name', 'icon', 'color', 'type', 'allocated', 'includeInTotal', 'alertAt'],
Debts: ['id', 'name', 'type', 'balance', 'interestRate', 'minPayment', 'icon', 'originalAmount', 'interestType', 'startDate', 'endDate', 'dueDate', 'lender', 'status'],
Settings: ['key', 'value'],
AssetsNonLiquid: ['id', 'title', 'category', 'subType', 'purchasePrice', 'currentValue', 'purchaseDate', 'location', 'icon', 'notes', 'specification', 'landArea', 'buildingArea', 'mfgYear', 'usefulLife', 'depreciationMethod', 'valuationReminder', 'lastValuationUpdate'],
Saham: ['ID', 'Title', 'Ticker', 'Shares', 'Avg. Cost', 'Current Price', 'Purchase Date', 'Location', 'Icon', 'Notes'],
Crypto: ['ID', 'Title', 'Ticker', 'Coins', 'Avg. Cost', 'Current Price', 'Purchase Date', 'Location', 'Icon', 'Notes'],
Reksadana: ['ID', 'Title', 'Ticker', 'Units', 'Avg. Cost', 'Current Price', 'Purchase Date', 'Location', 'Icon', 'Notes']
};
// ===================== HELPER =====================
/** Membaca semua data dari sebuah tab dan mengembalikannya sebagai array of objects */
function readSheet(sheetName) {
if (sheetName === 'Fixed Income Investment') {
return readFixedIncomeSheet();
}
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
if (!sheet) return [];
var data = sheet.getDataRange().getValues();
if (data.length <= 1) return [];
var headers = data[0];
var result = [];
for (var i = 1; i < data.length; i++) {
var row = data[i];
// Skip baris kosong (yang id/key-nya kosong)
if (!row[0] && row[0] !== 0) continue;
var obj = {};
for (var j = 0; j < headers.length; j++) {
obj[headers[j]] = row[j];
}
result.push(obj);
}
return result;
}
/** Helper untuk mengambil nilai secara case-insensitive dan alias kecocokan kolom */
function getValueCaseInsensitive(data, header) {
if (!data) return undefined;
// 1. Direct match
if (data[header] !== undefined) return data[header];
// 2. Normalisasikan header ke lowercase dan tanpa spasi/titik
var cleanHeader = header.toLowerCase().replace(/[^a-z0-9]/g, '');
// Daftar alias khusus untuk kolom tertentu
var aliases = {
'id': ['id', 'ID', 'key', 'Key'],
'title': ['title', 'Title', 'name', 'Name'],
'ticker': ['ticker', 'Ticker', 'symbol', 'Symbol'],
'shares': ['shares', 'Shares', 'qty', 'Qty', 'quantity', 'Quantity', 'units', 'Units', 'coins', 'Coins'],
'coins': ['shares', 'Shares', 'qty', 'Qty', 'quantity', 'Quantity', 'units', 'Units', 'coins', 'Coins'],
'units': ['shares', 'Shares', 'qty', 'Qty', 'quantity', 'Quantity', 'units', 'Units', 'coins', 'Coins'],
'avgcost': ['avgcost', 'avgCost', 'AvgCost', 'avg_cost', 'avg. cost', 'Avg. Cost', 'navperunit', 'navPerUnit', 'NavPerUnit'],
'navperunit': ['avgcost', 'avgCost', 'AvgCost', 'avg_cost', 'avg. cost', 'Avg. Cost', 'navperunit', 'navPerUnit', 'NavPerUnit'],
'currentprice': ['currentprice', 'currentPrice', 'CurrentPrice', 'current_price', 'current price', 'Current Price', 'currentnav', 'currentNav', 'CurrentNav', 'current_nav', 'nav'],
'currentnav': ['currentprice', 'currentPrice', 'CurrentPrice', 'current_price', 'current price', 'Current Price', 'currentnav', 'currentNav', 'CurrentNav', 'current_nav', 'nav'],
'purchasedate': ['purchasedate', 'purchaseDate', 'PurchaseDate', 'purchase_date', 'purchase date', 'Purchase Date', 'date', 'Date'],
'location': ['location', 'Location', 'broker', 'Broker', 'platform', 'Platform'],
'icon': ['icon', 'Icon'],
'notes': ['notes', 'Notes', 'keterangan', 'Keterangan']
};
// Cari di keys dari data
var keys = Object.keys(data);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var cleanKey = key.toLowerCase().replace(/[^a-z0-9]/g, '');
// Cocokkan clean header langsung
if (cleanKey === cleanHeader) return data[key];
// Cocokkan menggunakan alias
if (aliases[cleanHeader] && aliases[cleanHeader].indexOf(key) !== -1) {
return data[key];
}
}
return undefined;
}
/** Helper untuk membersihkan format ISO Date (dengan T dan Z) ke format YYYY-MM-DD standar */
function cleanDateString(dateStr) {
if (!dateStr) return '';
return String(dateStr).split('T')[0];
}
/** Menulis satu baris baru ke tab berdasarkan header yang sudah didefinisikan */
function appendToSheet(sheetName, data) {
if (sheetName === 'Fixed Income Investment') {
return appendFixedIncome(data);
}
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
if (!sheet) throw new Error('Tab "' + sheetName + '" tidak ditemukan');
var headers = HEADERS[sheetName];
var row = headers.map(function(h) {
var val = getValueCaseInsensitive(data, h);
return val !== undefined ? val : '';
});
sheet.appendRow(row);
}
/** Mengupdate baris berdasarkan id (kolom pertama) */
function updateInSheet(sheetName, id, data) {
if (sheetName === 'Fixed Income Investment') {
return updateFixedIncome(id, data);
}
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
if (!sheet) throw new Error('Tab "' + sheetName + '" tidak ditemukan');
var allData = sheet.getDataRange().getValues();
var headers = allData[0];
for (var i = 1; i < allData.length; i++) {
if (String(allData[i][0]) === String(id)) {
var rowValues = headers.map(function(h) {
var val = getValueCaseInsensitive(data, h);
return val !== undefined ? val : allData[i][headers.indexOf(h)];
});
sheet.getRange(i + 1, 1, 1, headers.length).setValues([rowValues]);
return true;
}
}
return false;
}
/** Menghapus baris berdasarkan id (kolom pertama) */
function deleteFromSheet(sheetName, id) {
if (sheetName === 'Fixed Income Investment') {
return deleteFixedIncome(id);
}
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
if (!sheet) throw new Error('Tab "' + sheetName + '" tidak ditemukan');
var allData = sheet.getDataRange().getValues();
for (var i = 1; i < allData.length; i++) {
if (String(allData[i][0]) === String(id)) {
sheet.deleteRow(i + 1);
return true;
}
}
return false;
}
// ===================== WEB APP ENDPOINTS =====================
/**
* GET — Membaca data.
* Param: ?sheet=Transactions (opsional, default semua)
* ?sheet=all (mengembalikan semua tab sekaligus)
*/
function doGet(e) {
try {
var sheetParam = (e && e.parameter && e.parameter.sheet) ? e.parameter.sheet : 'all';
var result = {};
if (sheetParam === 'all') {
// Kembalikan semua tab
for (var i = 0; i < VALID_SHEETS.length; i++) {
var name = VALID_SHEETS[i];
result[name] = readSheet(name);
}
} else if (VALID_SHEETS.indexOf(sheetParam) !== -1) {
result[sheetParam] = readSheet(sheetParam);
} else {
return ContentService.createTextOutput(JSON.stringify({ success: false, error: 'Tab tidak valid: ' + sheetParam }))
.setMimeType(ContentService.MimeType.JSON);
}
var activeSS = SpreadsheetApp.getActiveSpreadsheet();
return ContentService.createTextOutput(JSON.stringify({
success: true,
spreadsheetId: activeSS.getId(),
spreadsheetUrl: activeSS.getUrl(),
data: result
}))
.setMimeType(ContentService.MimeType.JSON);
} catch (error) {
return ContentService.createTextOutput(JSON.stringify({ success: false, error: error.message }))
.setMimeType(ContentService.MimeType.JSON);
}
}
/**
* POST — Menulis/Update/Hapus data.
* Body JSON: { action: "add|update|delete", sheet: "NamaTab", data: { ... } }
*
* Contoh:
* { action: "add", sheet: "Transactions", data: { id: "abc123", date: "2026-05-20", ... } }
* { action: "update", sheet: "Debts", data: { id: "xyz", balance: 5000000 } }
* { action: "delete", sheet: "Accounts", data: { id: "acc1" } }
*/
function doPost(e) {
try {
var payload;
if (e.postData && e.postData.contents) {
payload = JSON.parse(e.postData.contents);
} else {
return ContentService.createTextOutput(JSON.stringify({ success: false, error: 'No data received' }))
.setMimeType(ContentService.MimeType.JSON);
}
var action = payload.action;
var sheetName = payload.sheet;
var data = payload.data;
// Backward compatibility: support old "addTransaction" action
if (action === 'addTransaction') {
sheetName = 'Transactions';
action = 'add';
}
if (!sheetName || VALID_SHEETS.indexOf(sheetName) === -1) {
return ContentService.createTextOutput(JSON.stringify({ success: false, error: 'Tab tidak valid: ' + sheetName }))
.setMimeType(ContentService.MimeType.JSON);
}
if (action === 'add') {
appendToSheet(sheetName, data);
return ContentService.createTextOutput(JSON.stringify({ success: true, action: 'add', sheet: sheetName }))
.setMimeType(ContentService.MimeType.JSON);
} else if (action === 'update') {
var updated = updateInSheet(sheetName, data.id || data.key, data);
return ContentService.createTextOutput(JSON.stringify({ success: updated, action: 'update', sheet: sheetName }))
.setMimeType(ContentService.MimeType.JSON);
} else if (action === 'delete') {
var deleted = deleteFromSheet(sheetName, data.id || data.key);
return ContentService.createTextOutput(JSON.stringify({ success: deleted, action: 'delete', sheet: sheetName }))
.setMimeType(ContentService.MimeType.JSON);
} else {
return ContentService.createTextOutput(JSON.stringify({ success: false, error: 'Action tidak dikenali: ' + action }))
.setMimeType(ContentService.MimeType.JSON);
}
} catch (error) {
return ContentService.createTextOutput(JSON.stringify({ success: false, error: error.message }))
.setMimeType(ContentService.MimeType.JSON);
}
}
// ===================== SATU KALI SETUP & MIGRASI =====================
/**
* Menyiapkan tab baru 'AssetsNonLiquid' dan memindahkan data aset fisik dari 'Assets' secara otomatis.
* Jalankan fungsi ini sekali saja melalui tombol "Run" di editor Google Apps Script.
*/
function setupAssetsNonLiquidSheet() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
// 1. Dapatkan atau buat tab AssetsNonLiquid
var nonLiquidSheet = ss.getSheetByName('AssetsNonLiquid');
var isNewSheet = false;
if (!nonLiquidSheet) {
nonLiquidSheet = ss.insertSheet('AssetsNonLiquid');
isNewSheet = true;
}
// 2. Set Header
var headers = HEADERS.AssetsNonLiquid;
nonLiquidSheet.getRange(1, 1, 1, headers.length).setValues([headers]);
// Berikan format premium pada header
var headerRange = nonLiquidSheet.getRange(1, 1, 1, headers.length);
headerRange.setBackground('#0f5132') // Hijau Premium
.setFontColor('#ffffff') // Tulisan Putih
.setFontWeight('bold') // Tebal
.setHorizontalAlignment('center');
nonLiquidSheet.setFrozenRows(1); // Bekukan baris pertama
// Auto-resize kolom agar rapi
for (var i = 1; i <= headers.length; i++) {
nonLiquidSheet.autoResizeColumn(i);
}
// 3. Migrasi data dari tab Assets jika ada
var assetsSheet = ss.getSheetByName('Assets');
if (!assetsSheet) {
Logger.log('Tab Assets tidak ditemukan. Setup selesai dengan tab baru kosong.');
return 'Tab AssetsNonLiquid siap digunakan!';
}
var assetsData = assetsSheet.getDataRange().getValues();
if (assetsData.length <= 1) {
Logger.log('Tidak ada data di tab Assets. Setup selesai dengan tab baru kosong.');
return 'Tab AssetsNonLiquid siap digunakan!';
}
var assetsHeaders = assetsData[0];
var migratedCount = 0;
// Cari index kolom penting di Assets
var idxId = assetsHeaders.indexOf('id');
var idxTitle = assetsHeaders.indexOf('title');
var idxCategory = assetsHeaders.indexOf('category');
var idxSubType = assetsHeaders.indexOf('subType');
var idxPurchasePrice = assetsHeaders.indexOf('purchasePrice');
var idxCurrentValue = assetsHeaders.indexOf('currentValue');
var idxPurchaseDate = assetsHeaders.indexOf('purchaseDate');
var idxLocation = assetsHeaders.indexOf('location');
var idxIcon = assetsHeaders.indexOf('icon');
var idxNotes = assetsHeaders.indexOf('notes');
// Indeks baris yang akan dihapus dari tab Assets lama (mulai dari baris terbawah agar indeks tidak bergeser)
var rowsToDelete = [];
for (var r = 1; r < assetsData.length; r++) {
var row = assetsData[r];
var category = String(row[idxCategory]).toLowerCase();
// Kategori fisik yang harus dipindahkan: real-estat, kendaraan, koleksi
if (category === 'real-estat' || category === 'kendaraan' || category === 'koleksi') {
var id = row[idxId] || '';
var title = row[idxTitle] || '';
var purchasePrice = row[idxPurchasePrice] || 0;
var currentValue = row[idxCurrentValue] || 0;
var purchaseDate = row[idxPurchaseDate] || '';
var location = row[idxLocation] || '';
var icon = row[idxIcon] || '';
var notes = row[idxNotes] || '';
// Deteksi subtype dan properti khusus
var subType = '';
var depreciationMethod = 'Tidak Menyusut';
var usefulLife = 10;
if (category === 'real-estat') {
subType = 'rumah';
depreciationMethod = 'Tidak Menyusut'; // Properti umumnya naik/apresiasi
usefulLife = 20;
} else if (category === 'kendaraan') {
subType = 'mobil';
depreciationMethod = 'Garis Lurus'; // Kendaraan menyusut
usefulLife = 8;
} else if (category === 'koleksi') {
subType = 'elektronik';
depreciationMethod = 'Garis Lurus';
usefulLife = 4;
}
// Siapkan baris data baru untuk AssetsNonLiquid
var newRow = headers.map(function(h) {
if (h === 'id') return id;
if (h === 'title') return title;
if (h === 'category') return category;
if (h === 'subType') return subType;
if (h === 'purchasePrice') return purchasePrice;
if (h === 'currentValue') return currentValue;
if (h === 'purchaseDate') return purchaseDate;
if (h === 'location') return location;
if (h === 'icon') return icon;
if (h === 'notes') return notes;
if (h === 'depreciationMethod') return depreciationMethod;
if (h === 'usefulLife') return usefulLife;
return ''; // kolom spesifik area, mfgYear kosong dulu
});
nonLiquidSheet.appendRow(newRow);
rowsToDelete.push(r + 1); // Simpan baris Google Sheet (1-indexed)
migratedCount++;
}
}
// Hapus dari sheet Assets lama dari bawah ke atas agar indeks tidak geser
for (var d = rowsToDelete.length - 1; d >= 0; d--) {
assetsSheet.deleteRow(rowsToDelete[d]);
}
Logger.log('Migrasi Berhasil! ' + migratedCount + ' aset fisik dipindahkan ke AssetsNonLiquid.');
return 'Migrasi Berhasil! ' + migratedCount + ' aset fisik dipindahkan ke tab baru AssetsNonLiquid secara otomatis!';
}
// ===================== SATU KALI SETUP TAB BUDGETING =====================
/**
* Membuat tab 'Budgeting' dengan visualisasi premium 12 Bulan (Jan-Des), formula otomatis,
* dan pembagian 4 kelompok budget sesuai template Anda.
* Jalankan fungsi ini dari tombol "Run" di editor Google Apps Script.
*/
function setupBudgetingSheet() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
// A. Pastikan kategori pemasukan default terisi di tab BudgetCategories jika belum ada
var catSheet = ss.getSheetByName('BudgetCategories');
if (catSheet) {
var catValues = catSheet.getDataRange().getValues();
var existingCatNames = [];
if (catValues.length > 1) {
existingCatNames = catValues.slice(1).map(function(row) { return String(row[1]).toLowerCase(); });
}
var defaultIncomes = [
{ name: 'Salary', allocated: 9200000, icon: 'payments', color: '#4caf50' },
{ name: 'Partner Salary', allocated: 0, icon: 'payments', color: '#81c784' },
{ name: 'Side Hustle / Freelance', allocated: 0, icon: 'work', color: '#388e3c' },
{ name: 'Business Income', allocated: 0, icon: 'storefront', color: '#2e7d32' },
{ name: 'Investments Income / Dividends / Capital Gain', allocated: 479000, icon: 'trending_up', color: '#1b5e20' },
{ name: 'Rental Income', allocated: 15600000, icon: 'real_estate_agent', color: '#66bb6a' },
{ name: 'Commissions', allocated: 0, icon: 'paid', color: '#4caf50' },
{ name: 'Bonuses', allocated: 0, icon: 'redeem', color: '#ffb300' },
{ name: 'Pension Income', allocated: 0, icon: 'savings', color: '#8d6e63' },
{ name: 'Scholarships / Grants', allocated: 0, icon: 'school', color: '#1e88e5' },
{ name: 'Inheritance', allocated: 0, icon: 'family_restroom', color: '#ab47bc' },
{ name: 'Lottery / Gambling Winnings', allocated: 0, icon: 'casino', color: '#e53935' },
{ name: 'Gifts', allocated: 0, icon: 'featured_play_list', color: '#f48fb1' },
{ name: 'Refunds / Reimbursements', allocated: 0, icon: 'replay', color: '#90a4ae' },
{ name: 'Others', allocated: 0, icon: 'more_horiz', color: '#78909c' }
];
for (var d = 0; d < defaultIncomes.length; d++) {
var inc = defaultIncomes[d];
if (existingCatNames.indexOf(inc.name.toLowerCase()) === -1) {
var id = 'bcat_' + inc.name.toLowerCase().replace(/[^a-z0-9]/g, '_');
catSheet.appendRow([id, inc.name, inc.icon, inc.color, 'Pemasukan', inc.allocated, 'TRUE', 80]);
}
}
}
// 1. Buat atau dapatkan tab 'Budgeting'
var budgetSheet = ss.getSheetByName('Budgeting');
if (budgetSheet) {
ss.deleteSheet(budgetSheet); // Hapus jika sudah ada untuk menulis ulang layout secara rapi
}
budgetSheet = ss.insertSheet('Budgeting');
// 2. Baca data pendukung dari tab lain
var catData = readSheet('BudgetCategories');
var settingsData = readSheet('Settings');
// Cari custom monthly budgets di settings jika ada
var monthlyBudgets = {};
for (var i = 0; i < settingsData.length; i++) {
if (settingsData[i].key === 'monthlyBudgets') {
try {
monthlyBudgets = JSON.parse(settingsData[i].value);
} catch (e) {}
}
}
// 3. Setup Header & Kolom Bulan (E s/d P)
// Merge B2:D3 untuk judul Annual Budget
budgetSheet.getRange('B2:D3').merge()
.setValue('Annual Budget')
.setBackground('#1e293b')
.setFontColor('#ffffff')
.setFontWeight('bold')
.setFontSize(16)
.setHorizontalAlignment('center')
.setVerticalAlignment('middle');
var months = [
{ name: 'January', key: '01' }, { name: 'February', key: '02' }, { name: 'March', key: '03' },
{ name: 'April', key: '04' }, { name: 'May', key: '05' }, { name: 'June', key: '06' },
{ name: 'July', key: '07' }, { name: 'August', key: '08' }, { name: 'September', key: '09' },
{ name: 'October', key: '10' }, { name: 'November', key: '11' }, { name: 'December', key: '12' }
];
// Merge baris 2-3 untuk masing-masing bulan
for (var m = 0; m < months.length; m++) {
var colLetter = String.fromCharCode(69 + m); // E = 69, F = 70, dst.
var cellRange = colLetter + '2:' + colLetter + '3';
budgetSheet.getRange(cellRange).merge()
.setValue(months[m].name)
.setBackground('#1e293b')
.setFontColor('#ffffff')
.setFontWeight('bold')
.setFontSize(10)
.setHorizontalAlignment('center')
.setVerticalAlignment('middle');
}
// 4. Klasifikasi Kategori
var incomeCats = [];
var expenseCats = [];
var savingCats = [];
var investCats = [];
for (var c = 0; c < catData.length; c++) {
var name = catData[c].name;
var type = catData[c].type;
var id = catData[c].id;
var allocated = Number(catData[c].allocated) || 0;
var group = getCategoryGroup(name, type);
var item = { id: id, name: name, allocated: allocated };
if (group === 'INCOME') incomeCats.push(item);
else if (group === 'SAVINGS') savingCats.push(item);
else if (group === 'INVESTMENT') investCats.push(item);
else expenseCats.push(item);
}
// Helper fungsi pengelompokan kategori (sama dengan logika frontend)
function getCategoryGroup(name, type) {
if (type === 'Pemasukan') return 'INCOME';
var norm = name.toLowerCase();
if (norm.indexOf('saving') !== -1 || norm.indexOf('tabungan') !== -1 || norm.indexOf('darurat') !== -1 || norm.indexOf('emergency') !== -1 || norm.indexOf('sinking') !== -1) {
return 'SAVINGS';
}
if (norm.indexOf('invest') !== -1 || norm.indexOf('saham') !== -1 || norm.indexOf('crypto') !== -1 || norm.indexOf('reksa') !== -1 || norm.indexOf('bond') !== -1 || norm.indexOf('emas') !== -1 || norm.indexOf('gold') !== -1 || norm.indexOf('kripto') !== -1) {
return 'INVESTMENT';
}
return 'EXPENSES';
}
// 5. Tulis Baris KPI (Baris 4 & 5)
budgetSheet.getRange('B4:D4').merge().setValue('Allocated:').setFontWeight('bold').setHorizontalAlignment('right');
budgetSheet.getRange('B5:D5').merge().setValue('Left to allocate:').setFontWeight('bold').setHorizontalAlignment('right');
// 6. Gambar Konten & Formula
var currentRow = 6;
// a. Section Expected Income
var incomeStartRow = currentRow + 1;
budgetSheet.getRange(currentRow, 2, 1, 3).merge()
.setValue('💵 Expected Income (Pendapatan)')
.setFontWeight('bold');
budgetSheet.getRange(currentRow, 2, 1, 15)
.setBackground('#e8f5e9')
.setFontColor('#0f5132');
currentRow++;
for (var i = 0; i < incomeCats.length; i++) {
budgetSheet.getRange(currentRow, 2, 1, 3).merge().setValue(incomeCats[i].name).setFontWeight('bold');
// Tulis nilai bulan-bulanan
for (var m = 0; m < months.length; m++) {
var yearMonthKey = '2026-' + months[m].key;
var val = incomeCats[i].allocated;
if (monthlyBudgets[yearMonthKey] && monthlyBudgets[yearMonthKey][incomeCats[i].name] !== undefined) {
val = monthlyBudgets[yearMonthKey][incomeCats[i].name];
}
budgetSheet.getRange(currentRow, 5 + m).setValue(val).setNumberFormat('[$Rp-421] #,##0');
}
currentRow++;
}
var incomeEndRow = currentRow - 1;
// Baris Total Income
var totalIncomeRow = currentRow;
budgetSheet.getRange(currentRow, 2, 1, 3).merge().setValue('Total Income').setFontWeight('bold').setBackground('#f8f9fa');
for (var m = 0; m < months.length; m++) {
var colLetter = String.fromCharCode(69 + m);
var formula = '=SUM(' + colLetter + incomeStartRow + ':' + colLetter + incomeEndRow + ')';
budgetSheet.getRange(currentRow, 5 + m).setFormula(formula).setFontWeight('bold').setBackground('#f8f9fa').setNumberFormat('[$Rp-421] #,##0');
}
currentRow += 2; // Berikan jeda 1 baris
// b. Section Expenses
var expenseStartRow = currentRow + 1;
budgetSheet.getRange(currentRow, 2, 1, 3).merge()
.setValue('🛒 Expenses Budget (Belanja & Operasional)')
.setFontWeight('bold');
budgetSheet.getRange(currentRow, 2, 1, 15)
.setBackground('#fde8e8')
.setFontColor('#ba1a1a');
currentRow++;
for (var i = 0; i < expenseCats.length; i++) {
budgetSheet.getRange(currentRow, 2, 1, 3).merge().setValue(expenseCats[i].name);
for (var m = 0; m < months.length; m++) {
var yearMonthKey = '2026-' + months[m].key;
var val = expenseCats[i].allocated;
if (monthlyBudgets[yearMonthKey] && monthlyBudgets[yearMonthKey][expenseCats[i].name] !== undefined) {
val = monthlyBudgets[yearMonthKey][expenseCats[i].name];
}
budgetSheet.getRange(currentRow, 5 + m).setValue(val).setNumberFormat('[$Rp-421] #,##0');
}
currentRow++;
}
var expenseEndRow = currentRow - 1;
// Baris Total Expenses
var totalExpensesRow = currentRow;
budgetSheet.getRange(currentRow, 2, 1, 3).merge().setValue('Total Expenses').setFontWeight('bold').setBackground('#f8f9fa');
for (var m = 0; m < months.length; m++) {
var colLetter = String.fromCharCode(69 + m);
var formula = '=SUM(' + colLetter + expenseStartRow + ':' + colLetter + expenseEndRow + ')';
budgetSheet.getRange(currentRow, 5 + m).setFormula(formula).setFontWeight('bold').setBackground('#f8f9fa').setNumberFormat('[$Rp-421] #,##0');
}
currentRow += 2;
// c. Section Savings
var savingStartRow = currentRow + 1;
budgetSheet.getRange(currentRow, 2, 1, 3).merge()
.setValue('🐖 Saving Budget (Tabungan & Sinking Fund)')
.setFontWeight('bold');
budgetSheet.getRange(currentRow, 2, 1, 15)
.setBackground('#fffde7')
.setFontColor('#ef6c00');
currentRow++;
for (var i = 0; i < savingCats.length; i++) {
budgetSheet.getRange(currentRow, 2, 1, 3).merge().setValue(savingCats[i].name);
for (var m = 0; m < months.length; m++) {
var yearMonthKey = '2026-' + months[m].key;
var val = savingCats[i].allocated;
if (monthlyBudgets[yearMonthKey] && monthlyBudgets[yearMonthKey][savingCats[i].name] !== undefined) {
val = monthlyBudgets[yearMonthKey][savingCats[i].name];
}
budgetSheet.getRange(currentRow, 5 + m).setValue(val).setNumberFormat('[$Rp-421] #,##0');
}
currentRow++;
}
var savingEndRow = currentRow - 1;
// Baris Total Savings
var totalSavingsRow = currentRow;
budgetSheet.getRange(currentRow, 2, 1, 3).merge().setValue('Total Savings').setFontWeight('bold').setBackground('#f8f9fa');
for (var m = 0; m < months.length; m++) {
var colLetter = String.fromCharCode(69 + m);
var formula = '=SUM(' + colLetter + savingStartRow + ':' + colLetter + savingEndRow + ')';
budgetSheet.getRange(currentRow, 5 + m).setFormula(formula).setFontWeight('bold').setBackground('#f8f9fa').setNumberFormat('[$Rp-421] #,##0');
}
currentRow += 2;
// d. Section Investment
var investStartRow = currentRow + 1;
budgetSheet.getRange(currentRow, 2, 1, 3).merge()
.setValue('📈 Investment Budget (Investasi & Saham)')
.setFontWeight('bold');
budgetSheet.getRange(currentRow, 2, 1, 15)
.setBackground('#e3f2fd')
.setFontColor('#1565c0');
currentRow++;
for (var i = 0; i < investCats.length; i++) {
budgetSheet.getRange(currentRow, 2, 1, 3).merge().setValue(investCats[i].name);
for (var m = 0; m < months.length; m++) {
var yearMonthKey = '2026-' + months[m].key;
var val = investCats[i].allocated;
if (monthlyBudgets[yearMonthKey] && monthlyBudgets[yearMonthKey][investCats[i].name] !== undefined) {
val = monthlyBudgets[yearMonthKey][investCats[i].name];
}
budgetSheet.getRange(currentRow, 5 + m).setValue(val).setNumberFormat('[$Rp-421] #,##0');
}
currentRow++;
}
var investEndRow = currentRow - 1;
// Baris Total Investment
var totalInvestRow = currentRow;
budgetSheet.getRange(currentRow, 2, 1, 3).merge().setValue('Total Investment').setFontWeight('bold').setBackground('#f8f9fa');
for (var m = 0; m < months.length; m++) {
var colLetter = String.fromCharCode(69 + m);
var formula = '=SUM(' + colLetter + investStartRow + ':' + colLetter + investEndRow + ')';
budgetSheet.getRange(currentRow, 5 + m).setFormula(formula).setFontWeight('bold').setBackground('#f8f9fa').setNumberFormat('[$Rp-421] #,##0');
}
currentRow += 2;
// e. Section Summary KPI Bawah
// Investment as % of Income
var totalInvestPercentRow = currentRow;
budgetSheet.getRange(currentRow, 2, 1, 3).merge().setValue('Investment as % of Income').setFontWeight('bold').setBackground('#f8f9fa');
for (var m = 0; m < months.length; m++) {
var colLetter = String.fromCharCode(69 + m);
var formula = '=IFERROR(' + colLetter + totalInvestRow + '/' + colLetter + totalIncomeRow + ', 0)';
budgetSheet.getRange(currentRow, 5 + m).setFormula(formula).setFontWeight('bold').setBackground('#f8f9fa').setNumberFormat('0.0%');
}
currentRow++;
// Grand Total
var grandTotalRow = currentRow;
budgetSheet.getRange(currentRow, 2, 1, 3).merge().setValue('Grand Total (All Expense/Save/Invest)').setFontWeight('bold').setBackground('#e9ecef');
for (var m = 0; m < months.length; m++) {
var colLetter = String.fromCharCode(69 + m);
var formula = '=' + colLetter + totalExpensesRow + '+' + colLetter + totalSavingsRow + '+' + colLetter + totalInvestRow;
budgetSheet.getRange(currentRow, 5 + m).setFormula(formula).setFontWeight('bold').setBackground('#e9ecef').setNumberFormat('[$Rp-421] #,##0');
}
// 7. Tulis Rumus Atas (Allocated & Left to allocate di baris 4 & 5)
for (var m = 0; m < months.length; m++) {
var colLetter = String.fromCharCode(69 + m);
// Allocated: =IFERROR(GrandTotal / TotalIncome, 0)
var allocatedFormula = '=IFERROR(' + colLetter + grandTotalRow + '/' + colLetter + totalIncomeRow + ', 0)';
budgetSheet.getRange(4, 5 + m).setFormula(allocatedFormula).setNumberFormat('0.0%').setFontWeight('bold');
// Left to allocate: =TotalIncome - GrandTotal
var leftFormula = '=' + colLetter + totalIncomeRow + '-' + colLetter + grandTotalRow;
budgetSheet.getRange(5, 5 + m).setFormula(leftFormula).setNumberFormat('[$Rp-421] #,##0').setFontWeight('bold');
}
// 8. Freeze & Format Lebar Kolom
budgetSheet.setFrozenRows(5);
budgetSheet.setFrozenColumns(4); // Bekukan kolom A s/d D
budgetSheet.autoResizeColumn(2); // Kolom B (nama kategori)
budgetSheet.setColumnWidth(1, 20); // Kolom A (padding)
budgetSheet.setColumnWidth(3, 20); // Kolom C (padding)
budgetSheet.setColumnWidth(4, 20); // Kolom D (padding)
for (var m = 0; m < months.length; m++) {
budgetSheet.setColumnWidth(5 + m, 110); // Lebar sama rata untuk bulan
}
Logger.log('Tab Budgeting berhasil dibuat dengan formula & visualisasi premium!');
return 'Tab "Budgeting" tahunan dengan visualisasi premium berhasil dibuat langsung di Google Spreadsheet Anda!';
}
// ===================== SATU KALI SETUP & MIGRASI PENGINGAT =====================
/**
* Menyisipkan kolom 'valuationReminder' dan 'lastValuationUpdate' ke tab Accounts dan AssetsNonLiquid jika belum ada.
* Jalankan fungsi ini sekali saja melalui tombol "Run" di editor Google Apps Script.
*/
function migrateDatabaseForReminders() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetsToMigrate = ['Accounts', 'AssetsNonLiquid'];
for (var k = 0; k < sheetsToMigrate.length; k++) {
var sheetName = sheetsToMigrate[k];
var sheet = ss.getSheetByName(sheetName);
if (!sheet) continue;
var data = sheet.getDataRange().getValues();
if (data.length === 0) continue;
var headers = data[0].map(function(h) { return h.toString().trim(); });
var targetHeaders = HEADERS[sheetName];
// Cari header yang kurang
var missingHeaders = [];
for (var i = 0; i < targetHeaders.length; i++) {
if (headers.indexOf(targetHeaders[i]) === -1) {
missingHeaders.push(targetHeaders[i]);
}
}
if (missingHeaders.length > 0) {
// Tambahkan header yang kurang ke baris pertama
var lastCol = sheet.getLastColumn();
sheet.getRange(1, lastCol + 1, 1, missingHeaders.length).setValues([missingHeaders]);
// Jika ada baris data lama, isi nilai default
var lastRow = sheet.getLastRow();
if (lastRow > 1) {
for (var m = 0; m < missingHeaders.length; m++) {
var colNum = lastCol + 1 + m;
var missingHeader = missingHeaders[m];
var fillVal = '';
if (missingHeader === 'valuationReminder') {
fillVal = 'TRUE'; // default aktif
} else if (missingHeader === 'lastValuationUpdate') {
// Isi dengan tanggal hari ini atau default tanggal purchase/start
fillVal = new Date().toISOString().split('T')[0];
}
var range = sheet.getRange(2, colNum, lastRow - 1, 1);
var vals = [];
for (var r = 2; r <= lastRow; r++) {
vals.push([fillVal]);
}
range.setValues(vals);
}
}
Logger.log('Tab "' + sheetName + '" berhasil di-migrasi. Menambahkan kolom: ' + missingHeaders.join(', '));
} else {
Logger.log('Tab "' + sheetName + '" sudah up-to-date.');
}
}
return 'Migrasi Kolom Pengingat Selesai!';
}
/**
* Menyisipkan kolom-kolom komprehensif baru ke tab Debts.
* Jalankan fungsi ini sekali saja melalui tombol "Run" di editor Google Apps Script.
*/
function migrateDebtsColumns() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Debts');
if (!sheet) return 'Tab Debts tidak ditemukan';
var data = sheet.getDataRange().getValues();
if (data.length === 0) return 'Data kosong';
var headers = data[0].map(function(h) { return h.toString().trim(); });
var targetHeaders = HEADERS['Debts'];
// Cari header yang kurang
var missingHeaders = [];
for (var i = 0; i < targetHeaders.length; i++) {
if (headers.indexOf(targetHeaders[i]) === -1) {
missingHeaders.push(targetHeaders[i]);
}
}
if (missingHeaders.length > 0) {
// Tambahkan header yang kurang ke baris pertama
var lastCol = sheet.getLastColumn();
sheet.getRange(1, lastCol + 1, 1, missingHeaders.length).setValues([missingHeaders]);
// Jika ada baris data lama, isi nilai default
var lastRow = sheet.getLastRow();
if (lastRow > 1) {
for (var m = 0; m < missingHeaders.length; m++) {
var colNum = lastCol + 1 + m;
var missingHeader = missingHeaders[m];
var fillVal = '';
// Defaults:
if (missingHeader === 'status') {
fillVal = 'Aktif';
} else if (missingHeader === 'originalAmount') {
fillVal = '=INDIRECT("D"&ROW())'; // Assume originalAmount = balance for old rows to prevent division by zero
} else if (missingHeader === 'interestType') {
fillVal = 'Fixed/Flat';
} else if (missingHeader === 'startDate' || missingHeader === 'endDate') {
fillVal = new Date().toISOString().split('T')[0];
} else if (missingHeader === 'dueDate') {
fillVal = '1';
}
var range = sheet.getRange(2, colNum, lastRow - 1, 1);
var vals = [];
for (var r = 2; r <= lastRow; r++) {
vals.push([fillVal]);
}
range.setValues(vals);
}
}
Logger.log('Tab "Debts" berhasil di-migrasi. Menambahkan kolom: ' + missingHeaders.join(', '));
return 'Migrasi Kolom Debts Selesai! ' + missingHeaders.join(', ');
} else {
Logger.log('Tab "Debts" sudah up-to-date.');
return 'Tab Debts sudah up-to-date.';
}
}
// ===================== FIXED INCOME MULTI-TABLE HANDLERS =====================
/** Membaca data secara modular dari 3 seksi di tab Fixed Income Investment */
function readFixedIncomeSheet() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Fixed Income Investment');
if (!sheet) return [];
var result = [];
var lastRow = sheet.getLastRow();
if (lastRow < 31) return [];
var values = sheet.getRange(31, 1, lastRow - 30, 21).getValues();
// Baca header seksi masing-masing
var bondHeaders = sheet.getRange(30, 1, 1, 21).getValues()[0].map(function(h) { return h.toString().trim(); });
var depHeaders = sheet.getRange(79, 1, 1, 19).getValues()[0].map(function(h) { return h.toString().trim(); });
var p2pHeaders = sheet.getRange(119, 1, 1, 20).getValues()[0].map(function(h) { return h.toString().trim(); });
for (var i = 0; i < values.length; i++) {
var rowNum = i + 31;
var rowVal = values[i];
var id = rowVal[0];
if (!id) continue; // Skip baris tanpa ID
// Skip header rows or divider rows
var idStr = String(id).trim().toLowerCase();
if (idStr === 'id' || idStr === 'deposit type' || idStr === 'product name' || idStr === 'bond type') continue;
var obj = {};
var headers = bondHeaders;
var subType = 'sbn';
if (rowNum >= 80 && rowNum < 118) {
headers = depHeaders;
subType = 'deposito';
} else if (rowNum >= 120) {
headers = p2pHeaders;
subType = 'p2p';
}
for (var j = 0; j < headers.length; j++) {
if (headers[j]) {
obj[headers[j]] = rowVal[j];
}
}
// Normalisasi field kunci untuk sinkronisasi frontend
obj.id = id;
obj.subType = subType;
obj.category = 'investasi';
// Fallback pemetaan kolom dari template keuangan ke model frontend
obj.title = getValueCaseInsensitive(obj, 'Seri') || getValueCaseInsensitive(obj, 'Bond Type') || getValueCaseInsensitive(obj, 'Title') || getValueCaseInsensitive(obj, 'Product Name') || getValueCaseInsensitive(obj, 'Bank') || 'Aset Pendapatan Tetap';
obj.purchasePrice = Number(getValueCaseInsensitive(obj, 'Principal')) || 0;
obj.currentValue = subType === 'sbn' && Number(getValueCaseInsensitive(obj, 'Accrued Interest')) === 0 && Number(getValueCaseInsensitive(obj, 'Period Passed')) >= 24 ? 0 : obj.purchasePrice;
obj.purchaseDate = getValueCaseInsensitive(obj, 'Issue Date') || '';
obj.location = getValueCaseInsensitive(obj, 'Mitra Distribusi') || getValueCaseInsensitive(obj, 'Location') || getValueCaseInsensitive(obj, 'P2P Provider') || '';
var rate = Number(String(getValueCaseInsensitive(obj, 'Interest Rate (%)') || '').replace('%', '').trim()) || Number(String(getValueCaseInsensitive(obj, 'Coupon Type') || '').replace('%', '').trim()) || 6.4;
// Jika persentase dibaca sebagai fraksi desimal (misal 0.064 untuk 6.4%), kalikan dengan 100
if (rate > 0 && rate < 1) {
rate = rate * 100;
}
obj.interestRate = rate;
obj.maturityDate = getValueCaseInsensitive(obj, 'Maturity Date') || '';
obj.notes = getValueCaseInsensitive(obj, 'Notes') || '';
obj.icon = getValueCaseInsensitive(obj, 'Icon') || (subType === 'sbn' ? 'account_balance' : subType === 'deposito' ? 'savings' : 'show_chart');
result.push(obj);
}
return result;
}
/** Menyimpan data SBN/Deposito baru ke seksi tabel yang sesuai */
function appendFixedIncome(data) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Fixed Income Investment');
if (!sheet) throw new Error('Tab "Fixed Income Investment" tidak ditemukan');
var subType = data.subType || 'sbn';
var startRow = 31;
var endRow = 77;
var headerRow = 30;
var numCols = 21;
if (subType === 'deposito') {
startRow = 80;
endRow = 117;
headerRow = 79;
numCols = 19;
} else if (subType === 'p2p') {
startRow = 120;
endRow = 150;
headerRow = 119;
numCols = 20;
}
var headers = sheet.getRange(headerRow, 1, 1, numCols).getValues()[0].map(function(h) { return h.toString().trim(); });
// Temukan baris kosong pertama di seksi
var targetRow = -1;
for (var r = startRow; r <= endRow; r++) {
var idCell = sheet.getRange(r, 1).getValue();
if (!idCell) {
targetRow = r;
break;
}
}
if (targetRow === -1) {
targetRow = endRow;
sheet.insertRowAfter(endRow);
}
// Mapping data frontend ke kolom template
var mappedData = {};
mappedData.id = data.id || '';
mappedData.ID = data.id || '';
mappedData.Icon = data.icon || '';
mappedData.Notes = data.notes || '';
mappedData.Location = data.location || '';
if (subType === 'sbn') {
mappedData['Bond Type'] = data.bondType || data.fixedIncomeType || (data.title && data.title.includes('Sukuk') ? 'Sukuk Tabungan (ST)' : 'Obligasi Negara Ritel (ORI)');
mappedData['Seri'] = data.title || '';
mappedData['Issuer'] = data.issuer || 'SBSN Indonesia';
mappedData['Tenor (Year)'] = data.tenor !== undefined ? data.tenor : 2;
mappedData['Coupon Type'] = data.couponType || 'Floating with Floor';
mappedData['Interest Rate (%)'] = (data.interestRate !== undefined ? data.interestRate : 6.4) / 100;
mappedData['Tax'] = data.tax !== undefined ? data.tax : 0.10;
mappedData['Principal'] = data.purchasePrice || 0;
mappedData['Interest Payment Period'] = data.interestPaymentPeriod || 'Monthly';
mappedData['Mitra Distribusi'] = data.location || 'Bibit';
mappedData['Issue Date'] = cleanDateString(data.purchaseDate);
mappedData['Maturity Date'] = cleanDateString(data.maturityDate);
mappedData['Payment Date'] = data.paymentDate !== undefined ? data.paymentDate : 10;
} else if (subType === 'deposito') {
mappedData['Bank'] = data.title || '';
mappedData['Deposit Type'] = data.depositType || data.fixedIncomeType || 'Deposito Berjangka';
mappedData['Tenor'] = data.tenor !== undefined ? data.tenor : 12;
mappedData['Satuan Tenor'] = data.tenorUnit || 'Months';
mappedData['Interest Rate (%)'] = (data.interestRate !== undefined ? data.interestRate : 4.5) / 100;
mappedData['Tax'] = data.tax !== undefined ? data.tax : 0.20;
mappedData['Principal'] = data.purchasePrice || 0;
mappedData['Interest Payment Period'] = data.interestPaymentPeriod || 'Monthly';
mappedData['Issue Date'] = cleanDateString(data.purchaseDate);
mappedData['Maturity Date'] = cleanDateString(data.maturityDate);
mappedData['Payment Date'] = data.paymentDate !== undefined ? data.paymentDate : 10;
} else {
mappedData['P2P Provider'] = data.location || '';
mappedData['Product Name'] = data.title || '';
mappedData['Type'] = data.type || data.fixedIncomeType || 'Productive Loan';
mappedData['Tenor'] = data.tenor !== undefined ? data.tenor : 12;
mappedData['Satuan Tenor'] = data.tenorUnit || 'Months';
mappedData['Interest Rate (%)'] = (data.interestRate !== undefined ? data.interestRate : 12) / 100;
mappedData['Tax'] = data.tax !== undefined ? data.tax : 0.15;
mappedData['Principal'] = data.purchasePrice || 0;
mappedData['Interest Payment Period'] = data.interestPaymentPeriod || 'Monthly';
mappedData['Issue Date'] = cleanDateString(data.purchaseDate);
mappedData['Maturity Date'] = cleanDateString(data.maturityDate);
mappedData['Payment Date'] = data.paymentDate !== undefined ? data.paymentDate : 10;
}
var rowValues = headers.map(function(h) {
var val = mappedData[h] !== undefined ? mappedData[h] : getValueCaseInsensitive(data, h);
return val !== undefined ? val : '';
});
sheet.getRange(targetRow, 1, 1, headers.length).setValues([rowValues]);
// Tulis formula-formula dinamis template
if (subType === 'sbn') {
sheet.getRange(targetRow, 13).setFormula('=L' + targetRow + '*J' + targetRow + '*E' + targetRow + '*(1-K' + targetRow + ')'); // Expected Total Interest
sheet.getRange(targetRow, 15).setFormula('=IF(N' + targetRow + '="Monthly"; (L' + targetRow + '*J' + targetRow + '/12)*(1-K' + targetRow + '); (L' + targetRow + '*J' + targetRow + '/2)*(1-K' + targetRow + '))'); // Interest per Period
sheet.getRange(targetRow, 16).setFormula('=MIN(E' + targetRow + '*12; IFERROR(ROUND((TODAY()-G' + targetRow + ')/(365.25/12)); 0))'); // Period Passed
sheet.getRange(targetRow, 17).setFormula('=O' + targetRow + '*P' + targetRow + ''); // Total Interest Received
sheet.getRange(targetRow, 18).setFormula('=IF(P' + targetRow + '>=E' + targetRow + '*12; 0; O' + targetRow + ')'); // Accrued Interest
} else if (subType === 'deposito') {
sheet.getRange(targetRow, 12).setFormula('=K' + targetRow + '*I' + targetRow + '*(1-J' + targetRow + ')'); // Expected Total Interest
sheet.getRange(targetRow, 14).setFormula('=(K' + targetRow + '*I' + targetRow + '/12)*(1-J' + targetRow + ')'); // Interest per Period
sheet.getRange(targetRow, 15).setFormula('=MIN(D' + targetRow + '; IFERROR(ROUND((TODAY()-F' + targetRow + ')/(365.25/12)); 0))'); // Period Passed
sheet.getRange(targetRow, 16).setFormula('=N' + targetRow + '*O' + targetRow + ''); // Total Interest Received
sheet.getRange(targetRow, 17).setFormula('=IF(O' + targetRow + '>=D' + targetRow + '; 0; N' + targetRow + ')'); // Accrued Interest
} else {
sheet.getRange(targetRow, 12).setFormula('=K' + targetRow + '*I' + targetRow + '*(1-J' + targetRow + ')'); // Expected Total Interest
sheet.getRange(targetRow, 14).setFormula('=(K' + targetRow + '*I' + targetRow + '/12)*(1-J' + targetRow + ')'); // Interest per Period
sheet.getRange(targetRow, 15).setFormula('=MIN(D' + targetRow + '; IFERROR(ROUND((TODAY()-F' + targetRow + ')/(365.25/12)); 0))'); // Period Passed
sheet.getRange(targetRow, 16).setFormula('=N' + targetRow + '*O' + targetRow + ''); // Total Interest Received
sheet.getRange(targetRow, 17).setFormula('=IF(O' + targetRow + '>=D' + targetRow + '; 0; N' + targetRow + ')'); // Accrued Interest
}
}
/** Memperbarui data SBN/Deposito secara modular */
function updateFixedIncome(id, data) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Fixed Income Investment');
if (!sheet) return false;
var lastRow = sheet.getLastRow();
if (lastRow < 31) return false;
var ids = sheet.getRange(31, 1, lastRow - 30, 1).getValues();
for (var i = 0; i < ids.length; i++) {
if (String(ids[i][0]) === String(id)) {
var targetRow = i + 31;
var headerRow = 30;
var numCols = 21;
if (targetRow >= 80 && targetRow < 118) {
headerRow = 79;
numCols = 19;
} else if (targetRow >= 120) {
headerRow = 119;
numCols = 20;
}
var headers = sheet.getRange(headerRow, 1, 1, numCols).getValues()[0].map(function(h) { return h.toString().trim(); });
var currentRowValues = sheet.getRange(targetRow, 1, 1, headers.length).getValues()[0];
// Mapping updates
var mappedData = {};
if (data.title !== undefined) {
if (targetRow < 80) mappedData['Seri'] = data.title;
else if (targetRow < 118) mappedData['Bank'] = data.title;
else mappedData['Product Name'] = data.title;
}
if (data.purchasePrice !== undefined) mappedData['Principal'] = data.purchasePrice;
if (data.purchaseDate !== undefined) mappedData['Issue Date'] = cleanDateString(data.purchaseDate);
if (data.location !== undefined) {
if (targetRow < 80) mappedData['Mitra Distribusi'] = data.location;
else if (targetRow >= 120) mappedData['P2P Provider'] = data.location;
else mappedData['Location'] = data.location;
}
if (data.interestRate !== undefined) mappedData['Interest Rate (%)'] = data.interestRate / 100;
if (data.maturityDate !== undefined) mappedData['Maturity Date'] = cleanDateString(data.maturityDate);
if (data.notes !== undefined) mappedData['Notes'] = data.notes;
if (data.icon !== undefined) mappedData['Icon'] = data.icon;
var rowValues = headers.map(function(h, idx) {
if (h === 'id' || h === 'ID') return id;
var val = mappedData[h] !== undefined ? mappedData[h] : getValueCaseInsensitive(data, h);
return val !== undefined ? val : currentRowValues[idx];
});
sheet.getRange(targetRow, 1, 1, headers.length).setValues([rowValues]);
return true;
}
}
return false;
}
/** Menghapus data SBN/Deposito secara modular dengan mengosongkan sel baris agar tidak merusak baris */
function deleteFixedIncome(id) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Fixed Income Investment');
if (!sheet) return false;
var lastRow = sheet.getLastRow();
if (lastRow < 31) return false;
var ids = sheet.getRange(31, 1, lastRow - 30, 1).getValues();
for (var i = 0; i < ids.length; i++) {
if (String(ids[i][0]) === String(id)) {
var targetRow = i + 31;
var numCols = sheet.getLastColumn();
sheet.getRange(targetRow, 1, 1, numCols).clearContent();
return true;
}
}
return false;
}
// ===================== PROGRAMMATIC SETUP FOR FIXED INCOME DASHBOARD =====================
/**
* Membentuk visualisasi dashboard & tabel modular premium untuk tab Fixed Income Investment.
* Menyelamatkan data ST012 lama Anda dan menuliskannya kembali ke baris tabel baru.
* Jalankan fungsi ini sekali saja melalui tombol "Run" di editor Google Apps Script.
*/
function setupFixedIncomeDashboard() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Fixed Income Investment');
if (!sheet) {
sheet = ss.insertSheet('Fixed Income Investment');
}
// 1. BACKUP DATA LAMA
var backupData = [];
try {
var rawValues = sheet.getDataRange().getValues();
if (rawValues.length > 1) {
var headers = rawValues[0];
for (var r = 1; r < rawValues.length; r++) {
var row = rawValues[r];
if (row[0] && row[0] !== 'id' && row[0] !== 'ID') {
var obj = {};
for (var c = 0; c < headers.length; c++) {
obj[headers[c]] = row[c];
}
backupData.push(obj);
}
}
}
} catch (e) {
Logger.log('Tidak ada data lama untuk di-backup: ' + e.message);
}
// 2. CLEAR SHEET & SET THEME
sheet.clear();
// a. Lebar kolom padding A, C, D
sheet.setColumnWidth(1, 25); // Padding ID
sheet.setColumnWidth(3, 20); // Spacer
sheet.setColumnWidth(4, 20); // Spacer
// b. Judul Banner Utama (A1:T1)
sheet.getRange('A1:T1').merge()
.setValue('Fixed Income Investment')
.setBackground('#1e293b')
.setFontColor('#ffffff')
.setFontWeight('bold')
.setFontSize(16)
.setHorizontalAlignment('center')
.setVerticalAlignment('middle');
sheet.setRowHeight(1, 45);
// 3. KPI CARD KIRI (A3:D10)
sheet.getRange('B3:D3').merge().setValue('Interest Received').setFontWeight('bold').setFontColor('#475569').setFontSize(10).setHorizontalAlignment('left');
sheet.getRange('B4:D7').merge().setFormula('=SUM(Q31:Q77)+SUM(P80:P117)+SUM(Q120:Q150)')
.setFontWeight('bold').setFontColor('#0f5132').setFontSize(28).setHorizontalAlignment('left').setVerticalAlignment('middle').setNumberFormat('[$Rp-421] #,##0');
sheet.getRange('B8').setValue('Principal:').setFontWeight('bold').setFontColor('#475569');
sheet.getRange('C8:D8').merge().setFormula('=SUM(L31:L77)+SUM(K80:K117)+SUM(L120:L150)').setNumberFormat('[$Rp-421] #,##0').setFontWeight('bold');
sheet.getRange('B9').setValue('Expected Int:').setFontWeight('bold').setFontColor('#475569');
sheet.getRange('C9:D9').merge().setFormula('=SUM(M31:M77)+SUM(L80:L117)+SUM(M120:M150)').setNumberFormat('[$Rp-421] #,##0').setFontWeight('bold');
sheet.getRange('B10').setValue('Accrued Int:').setFontWeight('bold').setFontColor('#475569');
sheet.getRange('C10:D10').merge().setFormula('=SUM(R31:R77)+SUM(Q80:Q117)+SUM(R120:R150)').setNumberFormat('[$Rp-421] #,##0').setFontWeight('bold');
// Format border luar KPI Card
sheet.getRange('B3:D10').setBorder(true, true, true, true, null, null, '#cbd5e1', SpreadsheetApp.BorderStyle.SOLID);
// 4. SUMMARY TABLE TENGAH (F3:K23)
var summaryHeaders = ['Fixed Income Investment', 'Principal', 'Expected Total Interest', 'Total Interest Received', 'Accrued Interest'];
sheet.getRange(3, 6, 1, 5).setValues([summaryHeaders]).setBackground('#334155').setFontColor('#ffffff').setFontWeight('bold').setHorizontalAlignment('center');
// Baris Utama
sheet.getRange('F4').setValue('Bonds').setFontWeight('bold');
sheet.getRange('G4').setFormula('=SUM(L31:L77)').setNumberFormat('[$Rp-421] #,##0').setFontWeight('bold');
sheet.getRange('H4').setFormula('=SUM(M31:M77)').setNumberFormat('[$Rp-421] #,##0').setFontWeight('bold');
sheet.getRange('I4').setFormula('=SUM(Q31:Q77)').setNumberFormat('[$Rp-421] #,##0').setFontWeight('bold');
sheet.getRange('J4').setFormula('=SUM(R31:R77)').setNumberFormat('[$Rp-421] #,##0').setFontWeight('bold');
sheet.getRange('F4:J4').setBackground('#f1f5f9');
// Sub-detail Bonds
var bondsSubs = ['Obligasi FR', 'FR Syariah', 'Obligasi Negara Ritel (ORI)', 'Saving Bond Ritel (SBR)', 'Sukuk Ritel (SR)', 'Sukuk Tabungan (ST)'];
for (var i = 0; i < bondsSubs.length; i++) {
var r = 5 + i;
sheet.getRange(r, 6).setValue(' • ' + bondsSubs[i]);
sheet.getRange(r, 7).setFormula('=SUMIF(B31:B77; "' + bondsSubs[i] + '"; L31:L77)').setNumberFormat('[$Rp-421] #,##0');
sheet.getRange(r, 8).setFormula('=SUMIF(B31:B77; "' + bondsSubs[i] + '"; M31:M77)').setNumberFormat('[$Rp-421] #,##0');
sheet.getRange(r, 9).setFormula('=SUMIF(B31:B77; "' + bondsSubs[i] + '"; Q31:Q77)').setNumberFormat('[$Rp-421] #,##0');
sheet.getRange(r, 10).setFormula('=SUMIF(B31:B77; "' + bondsSubs[i] + '"; R31:R77)').setNumberFormat('[$Rp-421] #,##0');
sheet.getRange(r, 6, 1, 5).setFontColor('#475569');
}
sheet.getRange('F11').setValue('Time Deposit').setFontWeight('bold');
sheet.getRange('G11').setFormula('=SUM(K80:K117)').setNumberFormat('[$Rp-421] #,##0').setFontWeight('bold');
sheet.getRange('H11').setFormula('=SUM(L80:L117)').setNumberFormat('[$Rp-421] #,##0').setFontWeight('bold');
sheet.getRange('I11').setFormula('=SUM(P80:P117)').setNumberFormat('[$Rp-421] #,##0').setFontWeight('bold');
sheet.getRange('J11').setFormula('=SUM(Q80:Q117)').setNumberFormat('[$Rp-421] #,##0').setFontWeight('bold');
sheet.getRange('F11:J11').setBackground('#f1f5f9');
// Sub-detail Deposito
var depSubs = ['Deposito Berjangka', 'Sertifikat Deposito', 'Deposit on Call (DOC)'];
for (var i = 0; i < depSubs.length; i++) {
var r = 12 + i;
sheet.getRange(r, 6).setValue(' • ' + depSubs[i]);
sheet.getRange(r, 7).setFormula('=SUMIF(C80:C117; "' + depSubs[i] + '"; K80:K117)').setNumberFormat('[$Rp-421] #,##0');
sheet.getRange(r, 8).setFormula('=SUMIF(C80:C117; "' + depSubs[i] + '"; L80:L117)').setNumberFormat('[$Rp-421] #,##0');
sheet.getRange(r, 9).setFormula('=SUMIF(C80:C117; "' + depSubs[i] + '"; P80:P117)').setNumberFormat('[$Rp-421] #,##0');
sheet.getRange(r, 10).setFormula('=SUMIF(C80:C117; "' + depSubs[i] + '"; Q80:Q117)').setNumberFormat('[$Rp-421] #,##0');
sheet.getRange(r, 6, 1, 5).setFontColor('#475569');
}
sheet.getRange('F15').setValue('P2P Lending').setFontWeight('bold');
sheet.getRange('G15').setFormula('=SUM(L120:L150)').setNumberFormat('[$Rp-421] #,##0').setFontWeight('bold');
sheet.getRange('H15').setFormula('=SUM(M120:M150)').setNumberFormat('[$Rp-421] #,##0').setFontWeight('bold');
sheet.getRange('I15').setFormula('=SUM(Q120:Q150)').setNumberFormat('[$Rp-421] #,##0').setFontWeight('bold');
sheet.getRange('J15').setFormula('=SUM(R120:R150)').setNumberFormat('[$Rp-421] #,##0').setFontWeight('bold');
sheet.getRange('F15:J15').setBackground('#f1f5f9');
// Sub-detail P2P
var p2pSubs = ['Consumer Loan', 'Productive Loan'];
for (var i = 0; i < p2pSubs.length; i++) {
var r = 16 + i;
sheet.getRange(r, 6).setValue(' • ' + p2pSubs[i]);
sheet.getRange(r, 7).setFormula('=SUMIF(D120:D150; "' + p2pSubs[i] + '"; L120:L150)').setNumberFormat('[$Rp-421] #,##0');
sheet.getRange(r, 8).setFormula('=SUMIF(D120:D150; "' + p2pSubs[i] + '"; M120:M150)').setNumberFormat('[$Rp-421] #,##0');
sheet.getRange(r, 9).setFormula('=SUMIF(D120:D150; "' + p2pSubs[i] + '"; Q120:Q150)').setNumberFormat('[$Rp-421] #,##0');
sheet.getRange(r, 10).setFormula('=SUMIF(D120:D150; "' + p2pSubs[i] + '"; R120:R150)').setNumberFormat('[$Rp-421] #,##0');
sheet.getRange(r, 6, 1, 5).setFontColor('#475569');
}
// Total Grand Summary Row
sheet.getRange('F18').setValue('Total Fixed Income').setFontWeight('bold');
sheet.getRange('G18').setFormula('=G4+G11+G15').setNumberFormat('[$Rp-421] #,##0').setFontWeight('bold');
sheet.getRange('H18').setFormula('=H4+H11+H15').setNumberFormat('[$Rp-421] #,##0').setFontWeight('bold');
sheet.getRange('I18').setFormula('=I4+I11+I15').setNumberFormat('[$Rp-421] #,##0').setFontWeight('bold');
sheet.getRange('J18').setFormula('=J4+J11+J15').setNumberFormat('[$Rp-421] #,##0').setFontWeight('bold');
sheet.getRange('F18:J18').setBackground('#e2e8f0');
sheet.getRange('F3:J18').setBorder(true, true, true, true, true, true, '#cbd5e1', SpreadsheetApp.BorderStyle.SOLID);
// 5. SEKSI OBLIGASI/BONDS (Row 28 s/d 77)
sheet.getRange('B28:U28').merge().setValue('Bonds Holdings').setBackground('#1e293b').setFontColor('#ffffff').setFontWeight('bold').setFontSize(11).setVerticalAlignment('middle');
sheet.setRowHeight(28, 25);
sheet.getRange('A30:U30').clearContent();
sheet.getRange('A30:U30').setValues([['ID', 'Bond Type', 'Seri', 'Issuer', 'Tenor (Year)', 'Maturity Date', 'Issue Date', 'Payment Date', 'Coupon Type', 'Interest Rate (%)', 'Tax', 'Principal', 'Expected Total Interest', 'Interest Payment Period', 'Interest per Period (After Tax)', 'Period Passed', 'Total Interest Received', 'Accrued Interest', 'Mitra Distribusi', 'Icon', 'Notes']]);
sheet.getRange('A30:U30').setBackground('#475569').setFontColor('#ffffff').setFontWeight('bold').setHorizontalAlignment('center');
// 6. SEKSI DEPOSITO (Row 78 s/d 117)
sheet.getRange('B78:S78').merge().setValue('Time Deposit Holdings').setBackground('#1e293b').setFontColor('#ffffff').setFontWeight('bold').setFontSize(11).setVerticalAlignment('middle');
sheet.setRowHeight(78, 25);
sheet.getRange('A79:S79').setValues([['ID', 'Bank', 'Deposit Type', 'Tenor', 'Satuan Tenor', 'Issue Date', 'Maturity Date', 'Payment Date', 'Interest Rate (%)', 'Tax', 'Principal', 'Expected Total Interest', 'Interest Payment Period', 'Interest per Period (After Tax)', 'Period Passed', 'Total Interest Received', 'Accrued Interest', 'Icon', 'Notes']]);
sheet.getRange('A79:S79').setBackground('#475569').setFontColor('#ffffff').setFontWeight('bold').setHorizontalAlignment('center');
// 7. SEKSI P2P LENDING (Row 118 s/d 150)
sheet.getRange('B118:T118').merge().setValue('P2P Lending Holdings').setBackground('#1e293b').setFontColor('#ffffff').setFontWeight('bold').setFontSize(11).setVerticalAlignment('middle');
sheet.setRowHeight(118, 25);
sheet.getRange('A119:T119').setValues([['ID', 'P2P Provider', 'Product Name', 'Type', 'Tenor', 'Satuan Tenor', 'Issue Date', 'Maturity Date', 'Payment Date', 'Interest Rate (%)', 'Tax', 'Principal', 'Expected Total Interest', 'Interest Payment Period', 'Interest per Period (After Tax)', 'Period Passed', 'Total Interest Received', 'Accrued Interest', 'Icon', 'Notes']]);
sheet.getRange('A119:T119').setBackground('#475569').setFontColor('#ffffff').setFontWeight('bold').setHorizontalAlignment('center');
// Format Lebar Kolom data
sheet.setColumnWidth(2, 160); // Seri / Bank / P2P Provider
sheet.setColumnWidth(5, 75); // Tenor
sheet.setColumnWidth(6, 95); // Maturity Date
sheet.setColumnWidth(7, 95); // Issue Date
sheet.setColumnWidth(10, 85); // Interest Rate
sheet.setColumnWidth(12, 110); // Principal
sheet.setColumnWidth(13, 110); // Expected Total Interest
sheet.setColumnWidth(15, 110); // Interest per Period
sheet.setColumnWidth(17, 110); // Total Interest Received
sheet.setColumnWidth(18, 110); // Accrued Interest
// 8. TULIS KEMBALI BACKUP DATA LAMA KE TABEL BARU
var restoredCount = 0;
for (var k = 0; k < backupData.length; k++) {
var data = backupData[k];
var subType = data.subType || 'sbn';
var title = data.title || '';
if (title.toLowerCase().includes('deposito') || title.toLowerCase().includes('deposit') || title.toLowerCase().includes('bank') || data.category === 'deposito') {
subType = 'deposito';
} else if (title.toLowerCase().includes('st012') || title.toLowerCase().includes('sukuk') || title.toLowerCase().includes('sbn')) {
subType = 'sbn';
}
// Normalisasi detail untuk ST012
var finalData = {
id: data.id || 'ast_st012',
subType: subType,
title: title,
purchasePrice: Number(data.purchasePrice) || 100000000,
interestRate: Number(data.interestRate) || 6.4,
purchaseDate: data.purchaseDate ? data.purchaseDate.split('T')[0] : '2024-05-10',
maturityDate: data.maturityDate ? data.maturityDate.split('T')[0] : '2026-05-10',
location: data.location || 'Bibit',
notes: data.notes || 'Telah dicairkan ke Tabungan BCA',
icon: data.icon || 'account_balance'
};
appendFixedIncome(finalData);
restoredCount++;
}
// Jika tidak ada backup, buatkan satu ST012 default agar user langsung melihat contoh nyata
if (restoredCount === 0) {
var defaultST012 = {
id: 'ast_st012',
subType: 'sbn',
title: 'Sukuk Tabungan ST012-T2',
purchasePrice: 100000000,
interestRate: 6.4,
purchaseDate: '2024-05-10',
maturityDate: '2026-05-10',
location: 'Bibit',
notes: 'Telah dicairkan ke Tabungan BCA',
icon: 'account_balance'
};
appendFixedIncome(defaultST012);
}
// Set Frozen Row untuk mempermudah scroll di Bonds Holdings
sheet.setFrozenRows(1);
sheet.setFrozenColumns(0);
Logger.log('Tab Fixed Income Investment berhasil dibentuk persis dengan template Marcel!');
return 'Tab "Fixed Income Investment" berhasil dikonfigurasi menjadi dashboard modular premium dengan format Bonds, Time Deposit, dan P2P Lending secara otomatis!';
}
Di spreadsheet DompetKu Anda, klik menu Ekstensi → Apps Script.
Menu Ekstensi ada di baris menu atas, di antara "Alat" dan "Bantuan".
Klik di dalam kode → pilih semua (Ctrl/Cmd + A) → hapus → tempel kode terbaru → simpan (Ctrl/Cmd + S).
const HEADERS = { Transactions: ['id', 'date', 'desc', …], Saham: ['ID', 'Title', 'Ticker', …], Reksadana: ['ID', 'Title', 'Ticker', 'Units', …] // ← diperbaiki };
Tempel seluruh isi file kode terbaru — jangan hanya sebagian. Perhatikan baris Reksadana yang kini memiliki kolom Ticker.
Klik Deploy → Kelola deployment → ikon pensil ✏️ → pada Version pilih New version → Deploy.
Pilih New version pada deployment yang sudah ada — bukan "New deployment".
Karena Anda memakai New version, URL Web App tetap sama. Aplikasi DompetKu Anda langsung memakai kode terbaru.
Bug Reksadana sudah teratasi. Silakan kembali menggunakan DompetKu seperti biasa — tak ada langkah lain yang diperlukan.
Jangan pilih "New deployment". Membuat deployment baru akan menghasilkan URL Web App yang berbeda, sehingga Anda harus menghubungkan ulang aplikasi. Cukup New version pada deployment yang ada.
Tidak. Memperbarui Apps Script hanya mengganti kode program, bukan isi spreadsheet. Semua transaksi, aset, anggaran, dan utang Anda tetap utuh.
Sangat disarankan agar Anda ikut mendapat semua perbaikan & penyempurnaan. Namun jika Anda tidak mencatat Reksadana, dampak bug tidak Anda alami, sehingga update bisa dilakukan kapan pun Anda sempat.
Tidak perlu langkah ini. Cukup salin template DompetKu terbaru; semua perbaikan sudah termasuk di dalamnya.
Tombol Deploy berwarna biru di kanan atas editor Apps Script. Setelah diklik, pilih "Kelola deployment" (Manage deployments). Jika belum pernah men-deploy, pilih "New deployment" sekali, lalu untuk pembaruan berikutnya gunakan "New version".