// ════════════════════════════════════════════════════════════════════════
// HYBIEFLOWERS POS — Google Apps Script v4.0
// Deploy → New Deployment → Web App
// Execute as: Me | Who has access: Anyone
//
// WHATSAPP via Fonnte (fonnte.com) — GRATIS untuk personal use:
// 1. Daftar di fonnte.com → Connect nomor WA kamu (scan QR)
// 2. Salin API Token dari dashboard → isi di FONNTE_TOKEN di bawah
// 3. Selesai — notifikasi void otomatis ke 2 nomor WA
//
// EMAIL: otomatis via MailApp GAS (tidak perlu setup tambahan)
// ════════════════════════════════════════════════════════════════════════
// ── KONFIGURASI ──────────────────────────────────────────────────────────
// Isi token Fonnte kamu di sini setelah daftar di fonnte.com
var FONNTE_TOKEN = "ISI_TOKEN_FONNTE_KAMU_DI_SINI";
// Nomor WA penerima notifikasi void (format: 628xxx tanpa + atau spasi)
var WA_NUMBERS = ["6282280280188", "6285366404751"];
// Email penerima notifikasi void
var NOTIFY_EMAILS = ["hybieflowerss@gmail.com", "nicaza89@gmail.com", "kaidenkaizen22@gmail.com"];
// ─────────────────────────────────────────────────────────────────────────
var SHEET_SALES = "Penjualan";
var SHEET_PRODUCTS = "Produk";
var SHEET_VOID_LOG = "Void_Log";
var SHEET_SETTINGS = "POS_Settings";
var COLOR_PRIMARY = "#8b2233";
var COLOR_HEADER = "#ffffff";
var COLOR_ROW_ALT = "#fdf6f0";
// ════════════════════════════════════════════════════════════════════════
// MAIN HANDLERS
// ════════════════════════════════════════════════════════════════════════
function doPost(e) {
try {
var raw = (e.postData && e.postData.contents) ? e.postData.contents
: (e.parameter && e.parameter.payload) ? e.parameter.payload : "{}";
var data = JSON.parse(raw);
var ss = SpreadsheetApp.getActiveSpreadsheet();
// Test ping
if (data.id && String(data.id).indexOf("TEST") === 0)
return respond({ status:"ok", message:"Test diterima", test:true });
// Router
if (data.action === "save_settings") return saveSettings(ss, data.settings);
if (data.action === "void_notification") return handleVoidNotification(data);
// Default: simpan transaksi penjualan
saveSale(ss, data);
return respond({ status:"ok", id:data.id, total:data.total });
} catch (err) {
Logger.log("doPost ERROR: " + err + " | stack: " + err.stack);
return respond({ status:"error", message:err.toString() });
}
}
function doGet(e) {
if (e && e.parameter) {
var action = e.parameter.action;
if (action === "products") return getProducts();
if (action === "get_settings") return getSettingsData();
if (action === "get_history") return getHistoryData();
if (action === "next_tx_number") return getNextTxNumber();
}
return respond({ status:"ok", name:"Hybieflowers POS v4", time:new Date().toISOString() });
}
// ════════════════════════════════════════════════════════════════════════
// VOID NOTIFICATION — log + email + WhatsApp
// ════════════════════════════════════════════════════════════════════════
function handleVoidNotification(d) {
try {
var ss = SpreadsheetApp.getActiveSpreadsheet();
// 1. Catat di sheet Void_Log
var logSheet = ss.getSheetByName(SHEET_VOID_LOG);
if (!logSheet) {
logSheet = ss.insertSheet(SHEET_VOID_LOG);
var hdr = ["Waktu Void","No Transaksi","Pelanggan","Total (Rp)",
"Metode Bayar","Items","Waktu Transaksi","Kode Void","Operator"];
logSheet.appendRow(hdr);
logSheet.getRange(1,1,1,hdr.length)
.setBackground(COLOR_PRIMARY).setFontColor(COLOR_HEADER).setFontWeight("bold");
logSheet.setFrozenRows(1);
logSheet.setColumnWidth(1,170); logSheet.setColumnWidth(6,300);
}
var items = "";
try { items = JSON.parse(d.tx_items||"[]").join(", "); } catch(_) { items = String(d.tx_items||""); }
var totalFmt = "Rp " + Number(d.tx_total||0).toLocaleString("id-ID");
logSheet.appendRow([
d.voided_at || new Date().toLocaleString("id-ID"),
d.tx_id || "",
d.tx_customer|| "",
Number(d.tx_total)||0,
d.tx_payment || "",
items,
d.tx_datetime|| "",
d.void_code || "",
d.operator || ""
]);
// Format kolom Total sebagai Rp
var lr = logSheet.getLastRow();
logSheet.getRange(lr,4).setNumberFormat('"Rp "#,##0');
if (lr%2===0) logSheet.getRange(lr,1,1,9).setBackground("#fff5f5");
// 2. Kirim Email
var subject = "🚫 [VOID] Transaksi " + (d.tx_id||"") + " Dibatalkan — hybieflowers POS";
var txtBody =
"NOTIFIKASI PEMBATALAN TRANSAKSI\n" +
"=================================\n\n" +
"No Transaksi : " + (d.tx_id||"-") + "\n" +
"Pelanggan : " + (d.tx_customer||"-") + "\n" +
"Total : " + totalFmt + "\n" +
"Metode Bayar : " + (d.tx_payment||"-") + "\n" +
"Waktu TX : " + (d.tx_datetime||"-") + "\n" +
"Item : " + items + "\n\n" +
"Dibatalkan : " + (d.voided_at||"-") + "\n" +
"Kode Void : " + (d.void_code||"-") + "\n" +
"Operator : " + (d.operator||"-") + "\n\n" +
"— hybieflowers POS · Notifikasi Otomatis";
var htmlBody =
"
" +
"
" +
"
🚫 Transaksi DIBATALKAN " +
"
hybieflowers POS · Void Notification
" +
"
" +
"
" +
"No Transaksi " +
" "+(d.tx_id||"-")+" " +
"Pelanggan "+(d.tx_customer||"-")+" " +
"Total " +
" "+totalFmt+" " +
"Metode Bayar "+(d.tx_payment||"-")+" " +
"Waktu TX "+(d.tx_datetime||"-")+" " +
"Items "+items+" " +
"
" +
"
" +
"
" +
"Dibatalkan: "+(d.voided_at||"-")+" · " +
"Kode Void: "+(d.void_code||"-")+" · " +
"Operator: "+(d.operator||"-")+"
";
var emailsSent = 0;
NOTIFY_EMAILS.forEach(function(email) {
try {
MailApp.sendEmail({ to:email, subject:subject, body:txtBody, htmlBody:htmlBody });
emailsSent++;
} catch(mailErr) {
Logger.log("Email gagal ke " + email + ": " + mailErr);
}
});
// 3. Kirim WhatsApp via Fonnte
var waMsg =
"🚫 *VOID TRANSAKSI — hybieflowers POS*\n\n" +
"No Transaksi : *" + (d.tx_id||"-") + "*\n" +
"Pelanggan : " + (d.tx_customer||"-") + "\n" +
"Total : *" + totalFmt + "*\n" +
"Metode Bayar : " + (d.tx_payment||"-") + "\n" +
"Waktu TX : " + (d.tx_datetime||"-") + "\n" +
"Items : " + items + "\n\n" +
"⏰ Dibatalkan : " + (d.voided_at||"-") + "\n" +
"🔑 Kode Void : *" + (d.void_code||"-") + "*\n" +
"👤 Operator : " + (d.operator||"-") + "\n\n" +
"_Notifikasi otomatis · hybieflowers POS_";
var waSent = 0;
if (FONNTE_TOKEN && FONNTE_TOKEN !== "ISI_TOKEN_FONNTE_KAMU_DI_SINI") {
WA_NUMBERS.forEach(function(phone) {
try {
var options = {
method: "post",
headers: { "Authorization": FONNTE_TOKEN },
payload: { target: phone, message: waMsg },
muteHttpExceptions: true
};
var resp = UrlFetchApp.fetch("https://api.fonnte.com/send", options);
var result = JSON.parse(resp.getContentText());
if (result.status) waSent++;
else Logger.log("WA gagal ke " + phone + ": " + resp.getContentText());
} catch(waErr) {
Logger.log("WA error ke " + phone + ": " + waErr);
}
});
} else {
Logger.log("FONNTE_TOKEN belum diisi — WA dilewati");
}
return respond({ status:"ok", logged:true, emails_sent:emailsSent, wa_sent:waSent });
} catch(err) {
Logger.log("handleVoidNotification ERROR: " + err);
return respond({ status:"error", message:err.toString() });
}
}
// ════════════════════════════════════════════════════════════════════════
// SIMPAN TRANSAKSI PENJUALAN
// ════════════════════════════════════════════════════════════════════════
function saveSale(ss, d) {
var sheet = getOrCreateSheet(ss, SHEET_SALES);
if (sheet.getLastRow() === 0) {
var H = ["No Transaksi","Tanggal","Waktu","Pelanggan",
"Metode Bayar","Detail Item","Jml Item",
"Subtotal (Rp)","Diskon (Rp)","Total (Rp)","Catatan"];
sheet.appendRow(H);
styleHeaderRow(sheet, H.length);
sheet.setFrozenRows(1);
sheet.setColumnWidth(1,160); sheet.setColumnWidth(6,320);
}
var dtStr = d.datetime || "";
var parts = dtStr.split(" ");
var tglArr = (parts[0]||"").split("/");
var tanggal = tglArr.length===3
? tglArr[2]+"-"+tglArr[1]+"-"+tglArr[0] : (parts[0]||"");
sheet.appendRow([
d.id||"", tanggal, parts[1]||"",
d.customer||"Umum", d.payment||"Tunai",
d.items_summary||"", Number(d.item_count)||0,
Number(d.subtotal)||0, Number(d.discount)||0, Number(d.total)||0, d.note||""
]);
var lr = sheet.getLastRow();
if (lr%2===0) sheet.getRange(lr,1,1,11).setBackground(COLOR_ROW_ALT);
sheet.getRange(lr,8,1,3).setNumberFormat('"Rp "#,##0');
}
// ════════════════════════════════════════════════════════════════════════
// UPDATE RINGKASAN & HARIAN
// ════════════════════════════════════════════════════════════════════════
// ════════════════════════════════════════════════════════════════════════
// PRODUK
// ════════════════════════════════════════════════════════════════════════
function getProducts() {
try {
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sheet=ss.getSheetByName(SHEET_PRODUCTS);
if (!sheet||sheet.getLastRow()<2) return respond({products:[]});
var data=sheet.getDataRange().getValues();
var hdrs=data[0].map(function(h){return String(h).toLowerCase().trim();});
var products=[];
for (var i=1;i
1)?lr:1;
var txId="HBF-"+String(num).padStart(4,"0");
return respond({status:"ok",tx_id:txId});
} catch(err){ return respond({status:"error",message:err.toString()}); }
}
// ════════════════════════════════════════════════════════════════════════
// SETTINGS
// ════════════════════════════════════════════════════════════════════════
function saveSettings(ss, settingsObj) {
try {
var sheet=getOrCreateSheet(ss, SHEET_SETTINGS);
sheet.getRange("B:B").setNumberFormat("@STRING@");
sheet.clearContents();
sheet.appendRow(["key","value"]);
sheet.getRange("B:B").setNumberFormat("@STRING@");
Object.keys(settingsObj).forEach(function(k){
var strVal=(settingsObj[k]===null||settingsObj[k]===undefined)?'':String(settingsObj[k]);
sheet.appendRow([k, strVal]);
});
return respond({status:"ok"});
} catch(err){ return respond({status:"error",message:err.toString()}); }
}
function getSettingsData() {
try {
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sheet=ss.getSheetByName(SHEET_SETTINGS);
if (!sheet||sheet.getLastRow()<2) return respond({status:"ok",settings:{}});
var data=sheet.getDataRange().getValues();
var cfg={};
for (var i=1;i
📋 Salin Script
Mengerti, tutup