Langsung ke konten utama

kodegs sidev2

/* ========================= 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() : [] }; }

Komentar

Postingan populer dari blog ini

Index

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

Indext ppob

Dashboard Konter Dashboard Konter Cash & Bank — Saldo Awal Reload Omset Hari Ini Rp 0 0 transaksi Profit Hari Ini Rp 0 Admin/fee (akumulasi) Cari Tip: ketik “dana”, “setor”, “tarik”, atau catatan. Topup Setor/Tarik Riwayat Laporan (Range) Saldo CASH + BANK CASH Rp 0 BANK Rp 0 TOTAL ...