/* =========================
Dumai Konter - Code.gs (FULL FINAL)
FIX riwayat 0: pakai header mapping (anti kolom geser)
Spreadsheet: openById (webapp selalu baca yang benar)
Sheets:
- SETTINGS
- TRANSAKSI
- BARANG
- CUSTOMERS
- AR
========================= */
/** ✅ GANTI INI dengan ID spreadsheet kamu */
const SPREADSHEET_ID = "1qi4sI0jcM1HVzYT-Aqn5xtDXaOBjaGmOwi8TBfNsOSg";
const SHEET_SETTINGS = "SETTINGS";
const SHEET_TRX = "TRANSAKSI";
const SHEET_BARANG = "BARANG";
const SHEET_CUSTOMERS = "CUSTOMERS";
const SHEET_AR = "AR";
/* =========================
WEB APP
========================= */
function doGet(){
initSheets();
return HtmlService.createHtmlOutputFromFile("Index")
.setTitle("Dashboard Konter")
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
/* =========================
CORE HELPERS
========================= */
function _ss_(){
return SpreadsheetApp.openById(SPREADSHEET_ID);
}
function _tz_(){
return Session.getScriptTimeZone() || "Asia/Jakarta";
}
function _ymd_(d){
return Utilities.formatDate(new Date(d), _tz_(), "yyyy-MM-dd");
}
function _isSameDay_(a,b){
return _ymd_(a) === _ymd_(b);
}
function _toNum_(x){
const n = Number(x);
return isFinite(n) ? n : 0;
}
function _safeStr_(x){
return (x===null || x===undefined) ? "" : String(x);
}
function _clamp_(n,min,max){
n = _toNum_(n);
if (n < min) return min;
if (n > max) return max;
return n;
}
function _id_(){
const s = Utilities.getUuid().replace(/-/g,"").slice(0,10).toUpperCase();
return "C" + s;
}
function _getSheet_(name){
const ss = _ss_();
const sh = ss.getSheetByName(name);
if (!sh) throw new Error("Sheet tidak ditemukan: " + name);
return sh;
}
function _ensureSheet_(name){
const ss = _ss_();
let sh = ss.getSheetByName(name);
if (!sh) sh = ss.insertSheet(name);
return sh;
}
/* =========================
HEADER MAP (anti kolom geser)
========================= */
function _headerMap_(sh){
const lastCol = Math.max(1, sh.getLastColumn());
const headers = sh.getRange(1,1,1,lastCol).getValues()[0];
const map = {};
headers.forEach((h,i)=>{
const k = String(h||"").trim().toUpperCase();
if (k) map[k] = i;
});
return map;
}
function _requireCols_(map, required){
required.forEach(k=>{
if (map[k] === undefined) throw new Error("Kolom wajib hilang: " + k);
});
}
/* =========================
INIT SHEETS
========================= */
function initSheets(){
_getSettingsSheet_();
_getTrxSheet_();
_getBarangSheet_();
_getCustomersSheet_();
_getARSheet_();
return true;
}
/* =========================
SETTINGS
========================= */
function _getSettingsSheet_(){
const sh = _ensureSheet_(SHEET_SETTINGS);
if (sh.getLastRow() < 1){
sh.getRange(1,1,1,2).setValues([["KEY","VALUE"]]);
sh.getRange(2,1,5,2).setValues([
["SALDO_BANK", 0],
["SALDO_CASH", 0],
["SALDO_AWAL_BANK", 0],
["SALDO_AWAL_CASH", 0],
["LAST_INIT", new Date()]
]);
}
// pastikan header bener
const h = sh.getRange(1,1,1,2).getValues()[0].map(x=>String(x||"").trim().toUpperCase());
if (h[0] !== "KEY" || h[1] !== "VALUE"){
sh.clear();
sh.getRange(1,1,1,2).setValues([["KEY","VALUE"]]);
sh.getRange(2,1,5,2).setValues([
["SALDO_BANK", 0],
["SALDO_CASH", 0],
["SALDO_AWAL_BANK", 0],
["SALDO_AWAL_CASH", 0],
["LAST_INIT", new Date()]
]);
}
return sh;
}
function _getSetting_(key, def){
const sh = _getSettingsSheet_();
const v = sh.getDataRange().getValues();
for (let i=1;i reset
if (map["TS"] === undefined){
sh.clear();
sh.getRange(1,1,1,needHeader.length).setValues([needHeader]);
return sh;
}
// pastikan semua kolom ada (kalau kurang, tambahkan di akhir)
const lastCol = sh.getLastColumn();
const existing = sh.getRange(1,1,1,lastCol).getValues()[0].map(x=>String(x||"").trim().toUpperCase());
needHeader.forEach(h=>{
if (!existing.includes(h)){
sh.getRange(1, sh.getLastColumn()+1).setValue(h);
}
});
return sh;
}
function _appendTrx_(obj){
const sh = _getTrxSheet_();
const map = _headerMap_(sh);
// kolom minimal wajib
_requireCols_(map, ["TS","JENIS","DETAIL","CATATAN","D_CASH","D_BANK","TOTAL_BAYAR","PROFIT"]);
const row = new Array(sh.getLastColumn()).fill("");
row[map["TS"]] = obj.ts || new Date();
row[map["JENIS"]] = obj.jenis || "";
row[map["DETAIL"]] = obj.detail || "";
row[map["CATATAN"]] = obj.catatan || "";
row[map["D_CASH"]] = _toNum_(obj.d_cash);
row[map["D_BANK"]] = _toNum_(obj.d_bank);
row[map["TOTAL_BAYAR"]] = _toNum_(obj.total_bayar);
row[map["PROFIT"]] = _toNum_(obj.profit);
if (map["CUSTOMER_ID"] !== undefined) row[map["CUSTOMER_ID"]] = obj.customer_id || "";
if (map["PIUTANG_DELTA"] !== undefined) row[map["PIUTANG_DELTA"]] = _toNum_(obj.piutang_delta);
if (map["META_JSON"] !== undefined) row[map["META_JSON"]] = obj.meta_json || "";
sh.appendRow(row);
}
function _trxRowToObj_(map, r){
const get = (k)=> (map[k]===undefined ? "" : r[map[k]]);
const ts = get("TS");
return {
ts: (ts instanceof Date) ? ts.toISOString() : ts,
jenis: get("JENIS") || "",
detail: get("DETAIL") || "",
catatan: get("CATATAN") || "",
d_cash: _toNum_(get("D_CASH")),
d_bank: _toNum_(get("D_BANK")),
total_bayar: _toNum_(get("TOTAL_BAYAR")),
profit: _toNum_(get("PROFIT")),
customer_id: get("CUSTOMER_ID") || "",
piutang_delta: _toNum_(get("PIUTANG_DELTA"))
};
}
/* =========================
BARANG
========================= */
function _getBarangSheet_(){
const sh = _ensureSheet_(SHEET_BARANG);
if (sh.getLastRow() < 1){
sh.appendRow(["KODE","NAMA","STOK","MODAL","JUAL","UPDATED_AT"]);
}
return sh;
}
function _barangGetAll_(){
const sh = _getBarangSheet_();
const last = sh.getLastRow();
if (last <= 1) return [];
const rows = sh.getRange(2,1,last-1,6).getValues();
return rows
.filter(r=>_safeStr_(r[0]).trim())
.map(r=>{
const modal = _toNum_(r[3]);
const jual = _toNum_(r[4]);
return {
kode: _safeStr_(r[0]).trim(),
nama: _safeStr_(r[1]).trim(),
stok: _toNum_(r[2]),
modal,
jual,
profit: jual - modal
};
});
}
function _barangFindRow_(kode){
const sh = _getBarangSheet_();
const last = sh.getLastRow();
if (last <= 1) return { row: 0, data: null };
const vals = sh.getRange(2,1,last-1,6).getValues();
for (let i=0;i{
const cid = _safeStr_(r[1]).trim();
if (!cid) return;
const debit = _toNum_(r[4]);
const kredit = _toNum_(r[5]);
map[cid] = _toNum_(map[cid] || 0) + (debit - kredit);
});
return map;
}
function _getDebt_(customerId){
const m = _allDebtMap_();
return _toNum_(m[customerId] || 0);
}
function _arAppend_(row){
const sh = _getARSheet_();
sh.appendRow([
row.ts || new Date(),
row.customerId || "",
row.refJenis || "",
row.refTs || "",
_toNum_(row.debit),
_toNum_(row.kredit),
_toNum_(row.saldo),
row.catatan || ""
]);
}
function _customerList_(){
const sh = _getCustomersSheet_();
const last = sh.getLastRow();
if (last <= 1) return [];
const v = sh.getRange(2,1,last-1,6).getValues();
const list = v.map(r=>({
id: _safeStr_(r[0]).trim(),
nama: _safeStr_(r[1]).trim(),
hp: _safeStr_(r[2]).trim(),
alamat: _safeStr_(r[3]).trim(),
catatan: _safeStr_(r[4]).trim(),
})).filter(x=>x.id && x.nama);
const debtMap = _allDebtMap_();
list.forEach(x=> x.piutang = _toNum_(debtMap[x.id] || 0));
return list;
}
/* =========================
API: DASHBOARD
========================= */
function api_dashboard(){
initSheets();
const saldo = _getSaldo_();
const sh = _getTrxSheet_();
const last = sh.getLastRow();
let omset=0, profit=0, count=0;
if (last > 1){
const map = _headerMap_(sh);
_requireCols_(map, ["TS","JENIS","TOTAL_BAYAR","PROFIT"]);
const v = sh.getRange(2,1,last-1,sh.getLastColumn()).getValues();
const today = new Date();
v.forEach(r=>{
const ts = r[map["TS"]];
const jenis = _safeStr_(r[map["JENIS"]]).toUpperCase();
if (!(ts instanceof Date)) return;
if (!_isSameDay_(ts, today)) return;
if (jenis === "TOPUP_DANA" || jenis === "JUAL_BARANG"){
omset += _toNum_(r[map["TOTAL_BAYAR"]]);
profit += _toNum_(r[map["PROFIT"]]);
count += 1;
}
});
}
return { saldo, kpi:{ omset, profit, count } };
}
/* =========================
API: SALDO AWAL
========================= */
function api_getSaldoAwal(){
initSheets();
return {
bank: _toNum_(_getSetting_("SALDO_AWAL_BANK", 0)),
cash: _toNum_(_getSetting_("SALDO_AWAL_CASH", 0)),
};
}
function api_setSaldoAwal(data){
initSheets();
data = data || {};
const bank = _toNum_(data.bank);
const cash = _toNum_(data.cash);
const catatan = _safeStr_(data.catatan);
_setSetting_("SALDO_AWAL_BANK", bank);
_setSetting_("SALDO_AWAL_CASH", cash);
_setSaldo_(bank, cash);
_appendTrx_({
ts: new Date(),
jenis: "SALDO_AWAL",
detail: "Set saldo awal",
catatan,
d_cash: 0,
d_bank: 0,
total_bayar: 0,
profit: 0,
customer_id: "",
piutang_delta: 0,
meta_json: ""
});
return true;
}
/* =========================
API: SETOR / TARIK
========================= */
function api_setorTarik(data){
initSheets();
data = data || {};
const jenis = _safeStr_(data.jenis).toUpperCase();
const nominal = _toNum_(data.nominal);
const catatan = _safeStr_(data.catatan);
if (nominal <= 0) throw new Error("Nominal harus > 0");
let { bank, cash } = _getSaldo_();
let dCash = 0, dBank = 0;
if (jenis === "SETOR"){
dCash = -nominal; dBank = +nominal;
} else if (jenis === "TARIK"){
dBank = -nominal; dCash = +nominal;
} else {
throw new Error("Jenis harus SETOR atau TARIK");
}
bank += dBank; cash += dCash;
_setSaldo_(bank, cash);
_appendTrx_({
ts: new Date(),
jenis: "SETOR_TARIK",
detail: jenis,
catatan,
d_cash: dCash,
d_bank: dBank,
total_bayar: 0,
profit: 0,
customer_id: "",
piutang_delta: 0,
meta_json: JSON.stringify({ nominal })
});
return { bank, cash };
}
/* =========================
API: BARANG
========================= */
function api_barangUpsert(data){
initSheets();
data = data || {};
const kode = _safeStr_(data.kode).trim().toUpperCase();
const nama = _safeStr_(data.nama).trim();
const stok = _toNum_(data.stok);
const modal = _toNum_(data.modal);
const jual = _toNum_(data.jual);
if (!kode) throw new Error("Kode wajib");
if (!nama) throw new Error("Nama wajib");
const sh = _getBarangSheet_();
const f = _barangFindRow_(kode);
if (f.row){
sh.getRange(f.row, 1, 1, 6).setValues([[kode, nama, stok, modal, jual, new Date()]]);
} else {
sh.appendRow([kode, nama, stok, modal, jual, new Date()]);
}
return true;
}
function api_barangList(){
initSheets();
return _barangGetAll_();
}
/* =========================
PAYMENT HELPERS
========================= */
function _methodToAccount_(metode){
metode = _safeStr_(metode).toUpperCase();
if (metode === "CASH") return "CASH";
return "BANK";
}
function _calcTotalTagihan_(subtotal, diskon, ppnOn, biayaTambahan, ongkir){
subtotal = _toNum_(subtotal);
diskon = _toNum_(diskon);
biayaTambahan = _toNum_(biayaTambahan);
ongkir = _toNum_(ongkir);
if (diskon < 0) diskon = 0;
const dasar = Math.max(0, subtotal - diskon);
const ppn = ppnOn ? Math.round(dasar * 0.11) : 0;
const total = dasar + ppn + biayaTambahan + ongkir;
return { total, ppn };
}
function _normalizeIncoming_(totalTagihan, payload){
const bayarMasuk = _safeStr_((payload.extra||{}).bayar_masuk || "").toUpperCase();
const type = _safeStr_(payload.paymentType).toUpperCase();
let paid = _toNum_(payload.bayar);
if (type === "DEBT") paid = 0;
if (paid < 0) paid = 0;
if (paid > totalTagihan) paid = totalTagihan;
let payCash = 0, payBank = 0;
if (bayarMasuk === "CASH" || bayarMasuk === "BANK"){
if (bayarMasuk === "CASH") payCash = paid; else payBank = paid;
return { payCash, payBank, paidAmount: paid };
}
if (_safeStr_(payload.metode).toUpperCase() === "SPLIT" && Array.isArray(payload.split)){
let remaining = paid;
payload.split.forEach(s=>{
if (remaining <= 0) return;
const nom = _clamp_(_toNum_(s.nominal), 0, remaining);
remaining -= nom;
const acc = _methodToAccount_(s.metode);
if (acc === "CASH") payCash += nom; else payBank += nom;
});
if (remaining > 0) payBank += remaining;
return { payCash, payBank, paidAmount: paid };
}
const acc = _methodToAccount_(payload.metode);
if (acc === "CASH") payCash = paid; else payBank = paid;
return { payCash, payBank, paidAmount: paid };
}
function _applyPiutang_(customerId, refJenis, refTs, debit, kredit, catatan){
const before = _getDebt_(customerId);
const after = before + _toNum_(debit) - _toNum_(kredit);
_arAppend_({
ts: new Date(),
customerId,
refJenis,
refTs,
debit,
kredit,
saldo: after,
catatan: catatan || ""
});
return { before, after };
}
/* =========================
API: TOPUP DANA V2
========================= */
function api_topupDana_v2(payload){
initSheets();
payload = payload || {};
const extra = payload.extra || {};
const nominal = _toNum_(extra.nominal);
const admin = _toNum_(extra.admin);
const posisi = _safeStr_(extra.posisi).toUpperCase();
const modalDari = _safeStr_(extra.modal_dari).toUpperCase() || "BANK";
const bayarMasuk = _safeStr_(extra.bayar_masuk).toUpperCase() || "CASH";
if (nominal <= 0) throw new Error("Nominal harus > 0");
if (admin < 0) throw new Error("Admin tidak valid");
if (modalDari !== "CASH" && modalDari !== "BANK") throw new Error("Modal keluar dari harus CASH/BANK");
if (bayarMasuk !== "CASH" && bayarMasuk !== "BANK") throw new Error("Bayar masuk ke harus CASH/BANK");
const modalOut = (posisi === "DALAM") ? Math.max(0, nominal - admin) : nominal;
const subtotal = (posisi === "LUAR") ? (nominal + admin) : nominal;
const { total: totalTagihan } = _calcTotalTagihan_(
subtotal,
payload.diskon,
!!payload.ppnOn,
payload.biayaTambahan,
payload.ongkir
);
const paymentType = _safeStr_(payload.paymentType).toUpperCase();
const customerId = _safeStr_(payload.customerId).trim();
if ((paymentType === "PARTIAL" || paymentType === "DEBT") && !customerId){
throw new Error("Untuk PARTIAL/DEBT wajib pilih pelanggan.");
}
const { paidAmount } = _normalizeIncoming_(totalTagihan, payload);
let piutangDelta = 0;
if (paymentType === "PARTIAL") piutangDelta = totalTagihan - paidAmount;
if (paymentType === "DEBT") piutangDelta = totalTagihan;
let { bank, cash } = _getSaldo_();
let dCash = 0, dBank = 0;
if (modalDari === "CASH") dCash -= modalOut; else dBank -= modalOut;
if (bayarMasuk === "CASH") dCash += paidAmount; else dBank += paidAmount;
bank += dBank; cash += dCash;
_setSaldo_(bank, cash);
const ts = new Date();
const detail = `DANA pokok:${nominal} admin:${admin} posisi:${posisi} modal:${modalDari} masuk:${bayarMasuk}`;
const catatan = _safeStr_(payload.catatan);
_appendTrx_({
ts,
jenis: "TOPUP_DANA",
detail,
catatan,
d_cash: dCash,
d_bank: dBank,
total_bayar: totalTagihan,
profit: admin,
customer_id: customerId,
piutang_delta: piutangDelta,
meta_json: JSON.stringify({
paymentType,
subtotal,
totalTagihan,
paidAmount,
metode: payload.metode || "",
split: payload.split || null,
diskon: _toNum_(payload.diskon),
ppnOn: !!payload.ppnOn,
biayaTambahan: _toNum_(payload.biayaTambahan),
ongkir: _toNum_(payload.ongkir),
nominal, admin, posisi, modalDari, bayarMasuk
})
});
if (piutangDelta > 0){
_applyPiutang_(customerId, "TOPUP_DANA", ts, piutangDelta, 0, `Piutang TOPUP DANA. ${catatan}`);
}
return { ok:true, bank, cash, totalTagihan, paidAmount, piutangDelta };
}
/* =========================
API: JUAL BARANG V2
========================= */
function api_barangJual_v2(payload){
initSheets();
payload = payload || {};
const extra = payload.extra || {};
const kode = _safeStr_(extra.kode).trim().toUpperCase();
const qty = Math.max(1, _toNum_(extra.qty));
const modalDari = _safeStr_(extra.modal_dari).toUpperCase() || "BANK";
const bayarMasuk = _safeStr_(extra.bayar_masuk).toUpperCase() || "CASH";
if (!kode) throw new Error("Kode barang kosong");
if (modalDari !== "CASH" && modalDari !== "BANK") throw new Error("Modal keluar dari harus CASH/BANK");
if (bayarMasuk !== "CASH" && bayarMasuk !== "BANK") throw new Error("Bayar masuk ke harus CASH/BANK");
const f = _barangFindRow_(kode);
if (!f.row) throw new Error("Barang tidak ditemukan: "+kode);
const stok = _toNum_(f.data[2]);
const modal = _toNum_(f.data[3]);
const jual = _toNum_(f.data[4]);
const nama = _safeStr_(f.data[1]).trim();
if (stok < qty) throw new Error("Stok tidak cukup. Stok: "+stok);
const modalOut = modal * qty;
const subtotal = jual * qty;
const profit = (jual - modal) * qty;
const { total: totalTagihan } = _calcTotalTagihan_(
subtotal,
payload.diskon,
!!payload.ppnOn,
payload.biayaTambahan,
payload.ongkir
);
const paymentType = _safeStr_(payload.paymentType).toUpperCase();
const customerId = _safeStr_(payload.customerId).trim();
if ((paymentType === "PARTIAL" || paymentType === "DEBT") && !customerId){
throw new Error("Untuk PARTIAL/DEBT wajib pilih pelanggan.");
}
const { paidAmount } = _normalizeIncoming_(totalTagihan, payload);
let piutangDelta = 0;
if (paymentType === "PARTIAL") piutangDelta = totalTagihan - paidAmount;
if (paymentType === "DEBT") piutangDelta = totalTagihan;
let { bank, cash } = _getSaldo_();
let dCash = 0, dBank = 0;
if (modalDari === "CASH") dCash -= modalOut; else dBank -= modalOut;
if (bayarMasuk === "CASH") dCash += paidAmount; else dBank += paidAmount;
bank += dBank; cash += dCash;
_setSaldo_(bank, cash);
// update stok
const sh = _getBarangSheet_();
const stokSisa = stok - qty;
sh.getRange(f.row, 3).setValue(stokSisa);
sh.getRange(f.row, 6).setValue(new Date());
const ts = new Date();
const detail = `JUAL ${kode} (${nama}) qty:${qty} jual:${jual} modal:${modal} masuk:${bayarMasuk}`;
const catatan = _safeStr_(payload.catatan);
_appendTrx_({
ts,
jenis: "JUAL_BARANG",
detail,
catatan,
d_cash: dCash,
d_bank: dBank,
total_bayar: totalTagihan,
profit,
customer_id: customerId,
piutang_delta: piutangDelta,
meta_json: JSON.stringify({
paymentType,
subtotal,
totalTagihan,
paidAmount,
metode: payload.metode || "",
split: payload.split || null,
diskon: _toNum_(payload.diskon),
ppnOn: !!payload.ppnOn,
biayaTambahan: _toNum_(payload.biayaTambahan),
ongkir: _toNum_(payload.ongkir),
kode, nama, qty, modal, jual,
modalDari, bayarMasuk,
stokSisa
})
});
if (piutangDelta > 0){
_applyPiutang_(customerId, "JUAL_BARANG", ts, piutangDelta, 0, `Piutang JUAL BARANG. ${catatan}`);
}
return { ok:true, bank, cash, totalTagihan, paidAmount, piutangDelta, stokSisa, profit };
}
/* =========================
API: CUSTOMERS
========================= */
function api_listCustomers(){ initSheets(); return _customerList_(); }
function api_addCustomer(data){
initSheets();
data = data || {};
const nama = _safeStr_(data.nama).trim();
const hp = _safeStr_(data.hp).trim();
const alamat = _safeStr_(data.alamat).trim();
const catatan = _safeStr_(data.catatan).trim();
if (!nama) throw new Error("Nama pelanggan wajib");
const sh = _getCustomersSheet_();
const id = _id_();
sh.appendRow([id, nama, hp, alamat, catatan, new Date()]);
return { id };
}
function api_getCustomerDebt(data){
initSheets();
data = data || {};
const customerId = _safeStr_(data.customerId).trim();
if (!customerId) return { debt: 0 };
return { debt: _getDebt_(customerId) };
}
function api_payDebt(data){
initSheets();
data = data || {};
const customerId = _safeStr_(data.customerId).trim();
const bayar = _toNum_(data.bayar);
const metode = _safeStr_(data.metode).toUpperCase();
const catatan = _safeStr_(data.catatan);
if (!customerId) throw new Error("Customer wajib");
if (bayar <= 0) throw new Error("Nominal bayar harus > 0");
const before = _getDebt_(customerId);
if (before <= 0) throw new Error("Piutang pelanggan ini kosong.");
const bayarEfektif = Math.min(bayar, before);
let { bank, cash } = _getSaldo_();
let dCash = 0, dBank = 0;
const acc = _methodToAccount_(metode);
if (acc === "CASH") dCash += bayarEfektif; else dBank += bayarEfektif;
bank += dBank; cash += dCash;
_setSaldo_(bank, cash);
const ts = new Date();
_appendTrx_({
ts,
jenis: "BAYAR_PIUTANG",
detail: `Bayar piutang metode:${metode}`,
catatan,
d_cash: dCash,
d_bank: dBank,
total_bayar: bayarEfektif,
profit: 0,
customer_id: customerId,
piutang_delta: -bayarEfektif,
meta_json: JSON.stringify({ metode, before, bayarEfektif })
});
_applyPiutang_(customerId, "BAYAR_PIUTANG", ts, 0, bayarEfektif, `Pembayaran piutang. ${catatan}`);
return { ok:true, before, after: before-bayarEfektif };
}
/* =========================
API: RIWAYAT / LAPORAN
========================= */
function api_riwayatLast(limit){
initSheets();
limit = Math.max(1, _toNum_(limit || 60));
const sh = _getTrxSheet_();
const lastRow = sh.getLastRow();
if (lastRow < 2) return [];
const map = _headerMap_(sh);
_requireCols_(map, ["TS","JENIS","DETAIL","CATATAN","D_CASH","D_BANK","TOTAL_BAYAR","PROFIT"]);
const start = Math.max(2, lastRow - limit + 1);
const num = lastRow - start + 1;
const values = sh.getRange(start, 1, num, sh.getLastColumn()).getValues();
const rows = values.map(r=> _trxRowToObj_(map, r));
// sort terbaru di atas (aman walau ts string/iso)
rows.sort((a,b)=> new Date(b.ts) - new Date(a.ts));
return rows;
}
function api_laporanRange(data){
initSheets();
data = data || {};
const from = _safeStr_(data.from);
const to = _safeStr_(data.to);
if (!from || !to) throw new Error("From/To wajib");
const fromD = new Date(from + "T00:00:00");
const toD = new Date(to + "T23:59:59");
const sh = _getTrxSheet_();
const lastRow = sh.getLastRow();
if (lastRow < 2) return { rows: [], summary:{ omset:0, profit:0 } };
const map = _headerMap_(sh);
_requireCols_(map, ["TS","JENIS","TOTAL_BAYAR","PROFIT","DETAIL","CATATAN","D_CASH","D_BANK"]);
const v = sh.getRange(2,1,lastRow-1,sh.getLastColumn()).getValues();
const out = [];
let omset=0, profit=0;
v.forEach(r=>{
const ts = r[map["TS"]];
const d = (ts instanceof Date) ? ts : new Date(ts);
if (isNaN(d.getTime())) return;
if (d < fromD || d > toD) return;
const obj = _trxRowToObj_(map, r);
out.push(obj);
const j = _safeStr_(obj.jenis).toUpperCase();
if (j === "TOPUP_DANA" || j === "JUAL_BARANG"){
omset += _toNum_(obj.total_bayar);
profit += _toNum_(obj.profit);
}
});
// terbaru di atas
out.sort((a,b)=> new Date(b.ts) - new Date(a.ts));
return { rows: out, summary:{ omset, profit } };
}
/* fallback */
function api_submitPayment(){
throw new Error("Gunakan: api_topupDana_v2 / api_barangJual_v2.");
}
/* =========================
DEBUG (wajib return object)
========================= */
function api_debugTrx(){
initSheets();
const sh = _getTrxSheet_();
return {
ok:true,
spreadsheetId: SPREADSHEET_ID,
sheetName: sh.getName(),
lastRow: sh.getLastRow(),
lastCol: sh.getLastColumn(),
headers: sh.getRange(1,1,1,sh.getLastColumn()).getValues()[0],
sample: sh.getLastRow()>1 ? sh.getRange(1,1,Math.min(5,sh.getLastRow()),sh.getLastColumn()).getValues() : []
};
}
Dashboard Konter Dashboard Konter Cash & Bank Saldo Awal Topup DANA Setor/Tarik Riwayat Saldo CASH + BANK CASH Rp 0 BANK Rp 0 TOTAL Rp 0 Refresh Laporan Buka Laporan Filter tanggal + ringkasan profit & total transaksi. Saldo Awal Saldo Bank Saldo Cash Catatan: nilai ini jadi saldo dasar (bank & cash). Batal Simpan Topup DANA ...
Komentar
Posting Komentar