ขายสินค้า (POS)

ทำรายการขาย สแกนบาร์โค้ด และรับชำระเงิน

รายการสินค้าในตะกร้า

สินค้า ราคาหน่วย จำนวน รวม

ยังไม่มีสินค้าในตะกร้า สแกนบาร์โค้ดหรือเลือกสินค้าจำลองทางขวาได้เลย

ยอดรวม ฿0.00
ส่วนลดสมาชิก ฿0.00
ส่วนลดโปรโมชัน ฿0.00
ยอดชำระสุทธิ ฿0.00

คลิกเพื่อจำลองสแกนสินค้า (Barcode Simulator)

รายการสินค้าทั้งหมด 0 รายการ
สินค้าสต๊อกต่ำกว่ากำหนด 0 รายการ
มูลค่าสินค้าในคลัง (ราคาทุน) ฿0.00

คลังเก็บสินค้าและสต๊อก

รหัสสินค้า บาร์โค้ด ชื่อสินค้า หมวดหมู่ ราคาทุน ราคาขาย 1 ราคาขาย 2 คงเหลือ หน่วย สต๊อกขั้นต่ำ การจัดการ

ข้อมูลเอกสารการรับเข้า

ยอดรวมสินค้า ฿0.00
ส่วนลดสินค้า -฿0.00
บวกเพิ่มอื่นๆ ฿0.00
มูลค่าก่อน VAT ฿0.00
ภาษี VAT (0%) ฿0.00
ราคารวมสุทธิ ฿0.00

รายการสินค้ารับเข้า

สินค้า / รายละเอียด จำนวน ทุน/หน่วย (฿) ส่วนลด (฿) ราคารวม
ยอดหนี้ค้างชำระทั้งหมด ฿0.00
จำนวนเจ้าหนี้ในระบบ 0 ราย

รายชื่อคู่ค้า / เจ้าหนี้การค้า

ชื่อผู้จำหน่าย / บริษัท ข้อมูลติดต่อ / เบอร์โทร ยอดค้างชำระปัจจุบัน ประวัติธุรกรรม จัดการหนี้

ทะเบียนข้อมูลสมาชิก

รหัสสมาชิก ชื่อสมาชิก เบอร์โทรศัพท์ ระดับราคาขาย คะแนนสะสม การจัดการ

ตั้งค่าโปรโมชันสินค้า

ชื่อโปรโมชัน ประเภทโปรโมชัน เงื่อนไขสินค้า สิทธิ์พิเศษ / ของแถม สถานะการใช้งาน ลบ

ประวัติการทำรายการและประวัติต่างๆ

เลือกวันที่:

ตั้งค่าโปรแกรมและการเชื่อมโยง Google Sheets

ข้อมูลร้านค้า (สำหรับหัวใบเสร็จและใบเสนอราคา)

ตั้งค่า QR Code ช่องทางชำระเงินโอน

การจัดการฐานข้อมูลคอมพิวเตอร์หน้าร้าน (IndexedDB)

การเชื่อมต่อ Google Sheets API (ซิงค์ขึ้นเว็บรายงาน)

นำ URL เว็บแอปที่ได้จากการเผยแพร่สคริปต์ใน Google Sheets มาใส่ในช่องนี้

💡 ขั้นตอนการตั้งค่า Google Sheets เพื่อใช้เป็นฐานข้อมูลคลาวด์:

  1. เปิด Google Sheets ใหม่ของคุณขึ้นมา
  2. ไปที่เมนู **ส่วนขยาย (Extensions)** > **Apps Script**
  3. ลบโค้ดเริ่มต้นออก แล้วคัดลอกโค้ดด้านล่างนี้ไปวางแทนที่:
📜 โค้ด Apps Script ฉบับสมบูรณ์ (ความเร็วสูง 100 เท่า & ซิงค์ครบทุกเมนู):
// โค้ด Apps Script สำหรับวางใน Google Sheets ของคุณ (ระบบซิงค์คลาวด์ 100% ครบทุกเมนู)
// (กดปุ่มเรียกใช้ฟังก์ชัน initSheets 1 ครั้ง เพื่อสร้างแผ่นงานทั้ง 7 แผ่นงานและหัวตารางให้อัตโนมัติ)
function formatSheetAsTable(sheetTab, headerBgColor) {
  var lastRow = sheetTab.getLastRow();
  var lastCol = sheetTab.getLastColumn();
  if (lastRow === 0 || lastCol === 0) return;

  sheetTab.setFrozenRows(1);
  var headerRange = sheetTab.getRange(1, 1, 1, lastCol);
  headerRange.setBackground(headerBgColor || "#059669")
             .setFontColor("#FFFFFF")
             .setFontWeight("bold")
             .setHorizontalAlignment("center")
             .setVerticalAlignment("middle")
             .setFontSize(11);

  sheetTab.setRowHeight(1, 34);

  if (lastRow > 1) {
    var bandings = sheetTab.getBandings();
    for (var b = 0; b < bandings.length; b++) {
      bandings[b].remove();
    }
    var dataRange = sheetTab.getRange(1, 1, lastRow, lastCol);
    dataRange.applyRowBanding(SpreadsheetApp.BandingTheme.LIGHT_GREY, true, false);
    
    var bodyRange = sheetTab.getRange(2, 1, lastRow - 1, lastCol);
    bodyRange.setVerticalAlignment("middle").setFontSize(10);
  }

  for (var c = 1; c <= lastCol; c++) {
    sheetTab.autoResizeColumn(c);
  }
}

function initSheets() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet();
  
  var salesSheet = sheet.getSheetByName("Sales") || sheet.insertSheet("Sales");
  if (salesSheet.getLastRow() === 0) {
    salesSheet.appendRow(["เลขที่บิล", "วันที่", "สมาชิก", "ยอดขายสุทธิ", "ส่วนลด", "กำไรเบื้องต้น", "การชำระเงิน"]);
  }
  formatSheetAsTable(salesSheet, "#059669");
  
  var prodSheet = sheet.getSheetByName("Products") || sheet.insertSheet("Products");
  if (prodSheet.getLastRow() === 0) {
    prodSheet.appendRow(["บาร์โค้ด", "ชื่อสินค้า", "หมวดหมู่", "ราคาทุน", "ราคาปกติ", "ราคาสมาชิก", "สต๊อก", "สต๊อกขั้นต่ำ", "หน่วยนับ"]);
  }
  formatSheetAsTable(prodSheet, "#2563EB");
  
  var memSheet = sheet.getSheetByName("Members") || sheet.insertSheet("Members");
  if (memSheet.getLastRow() === 0) {
    memSheet.appendRow(["รหัสสมาชิก", "ชื่อ-นามสกุล", "เบอร์โทรศัพท์", "ระดับราคา", "คะแนนสะสม"]);
  }
  formatSheetAsTable(memSheet, "#7C3AED");

  var intakeSheet = sheet.getSheetByName("Intakes") || sheet.insertSheet("Intakes");
  if (intakeSheet.getLastRow() === 0) {
    intakeSheet.appendRow(["เลขที่บันทึก", "เลขที่ใบรับสินค้า/Invoice", "วันที่", "เจ้าหนี้/คู่ค้า", "ยอดเงินรวมสุทธิ (฿)", "ประเภทภาษี (VAT)", "ภาษีมูลค่าเพิ่ม (฿)", "การชำระเงิน"]);
  }
  formatSheetAsTable(intakeSheet, "#0891B2");

  var credSheet = sheet.getSheetByName("Creditors") || sheet.insertSheet("Creditors");
  if (credSheet.getLastRow() === 0) {
    credSheet.appendRow(["รหัสเจ้าหนี้", "ชื่อบริษัท/คู่ค้า", "เบอร์โทรติดต่อ", "ยอดหนี้ค้างชำระ (฿)"]);
  }
  formatSheetAsTable(credSheet, "#EA580C");

  var wdrSheet = sheet.getSheetByName("Withdrawals") || sheet.insertSheet("Withdrawals");
  if (wdrSheet.getLastRow() === 0) {
    wdrSheet.appendRow(["เลขที่เบิก", "วันที่", "ชื่อสินค้า", "จำนวน", "หน่วยนับ", "เหตุผลการเบิก", "หมายเหตุ"]);
  }
  formatSheetAsTable(wdrSheet, "#DC2626");

  var promoSheet = sheet.getSheetByName("Promotions") || sheet.insertSheet("Promotions");
  if (promoSheet.getLastRow() === 0) {
    promoSheet.appendRow(["รหัสโปรโมชัน", "ชื่อโปรโมชัน", "ประเภท", "เงื่อนไข/ส่วนลด"]);
  }
  formatSheetAsTable(promoSheet, "#D97706");
  
  SpreadsheetApp.getUi().alert("จัดฟอร์แมตแผ่นงานทั้ง 7 แผ่นงานเป็นตารางสวยงามพร้อมใช้งานเรียบร้อยแล้วครับ!");
}

function doGet(e) {
  var action = e && e.parameter ? e.parameter.action : "";
  var sheet = SpreadsheetApp.getActiveSpreadsheet();

  if (action === "get_all_data" || action === "get_products_members") {
    var prodSheet = sheet.getSheetByName("Products");
    var products = [];
    if (prodSheet) {
      var prodData = prodSheet.getDataRange().getValues();
      for (var i = 1; i < prodData.length; i++) {
        if (prodData[i][0]) {
          products.push({
            productCode: String(prodData[i][0]),
            barcode: String(prodData[i][0]),
            name: String(prodData[i][1] || ""),
            category: String(prodData[i][2] || "ทั่วไป"),
            cost: Number(prodData[i][3]) || 0,
            price1: Number(prodData[i][4]) || 0,
            price2: Number(prodData[i][5]) || 0,
            stock: Number(prodData[i][6]) || 0,
            minStock: Number(prodData[i][7]) || 0,
            unit: String(prodData[i][8] || "ชิ้น")
          });
        }
      }
    }
    
    var memSheet = sheet.getSheetByName("Members");
    var members = [];
    if (memSheet) {
      var memData = memSheet.getDataRange().getValues();
      for (var j = 1; j < memData.length; j++) {
        if (memData[j][0]) {
          members.push({
            id: String(memData[j][0]),
            name: String(memData[j][1] || ""),
            phone: String(memData[j][2] || ""),
            priceTier: String(memData[j][3] || "ราคาส่ง 1"),
            points: Number(memData[j][4]) || 0
          });
        }
      }
    }

    var credSheet = sheet.getSheetByName("Creditors");
    var creditors = [];
    if (credSheet) {
      var credData = credSheet.getDataRange().getValues();
      for (var c = 1; c < credData.length; c++) {
        if (credData[c][0]) {
          creditors.push({
            id: String(credData[c][0]),
            name: String(credData[c][1] || ""),
            contact: String(credData[c][2] || ""),
            balance: Number(credData[c][3]) || 0
          });
        }
      }
    }
    
    return ContentService.createTextOutput(JSON.stringify({
      status: "success",
      products: products,
      members: members,
      creditors: creditors
    })).setMimeType(ContentService.MimeType.JSON);
  }

  return ContentService.createTextOutput(JSON.stringify({
    status: "success",
    message: "Google Apps Script Web App สำหรับ PennuengPOS ทำงานปกติพร้อมใช้งานครับ!"
  })).setMimeType(ContentService.MimeType.JSON);
}

function doPost(e) {
  if (!e || !e.postData || !e.postData.contents) {
    return ContentService.createTextOutput(JSON.stringify({
      status: "error",
      message: "ไม่พบข้อมูล POST (กรณีนี้เกิดขึ้นเมื่อกดปุ่ม 'เรียกใช้' ในตัวแก้ไข Apps Script โดยตรง ให้ทดสอบนำ URL ที่ได้จากการปรับใช้ Web App ไปใส่ใน PennuengPOS เพื่อใช้งานครับ)"
    })).setMimeType(ContentService.MimeType.JSON);
  }

  var requestData = JSON.parse(e.postData.contents);
  var action = requestData.action;
  var sheet = SpreadsheetApp.getActiveSpreadsheet();
  
  if (action === "sync_sale") {
    var salesSheet = sheet.getSheetByName("Sales") || sheet.insertSheet("Sales");
    if (salesSheet.getLastRow() === 0) {
      salesSheet.appendRow(["เลขที่บิล", "วันที่", "สมาชิก", "ยอดขายสุทธิ", "ส่วนลด", "กำไรเบื้องต้น", "การชำระเงิน"]);
    }
    var sale = requestData.data;
    var data = salesSheet.getDataRange().getValues();
    var rowIndex = -1;
    for (var sIdx = 1; sIdx < data.length; sIdx++) {
      if (String(data[sIdx][0]) === String(sale.id)) {
        rowIndex = sIdx + 1;
        break;
      }
    }
    var rowVal = [sale.id, sale.date, sale.memberName || "ลูกค้าทั่วไป", sale.total, sale.discount, sale.profit, sale.paymentType];
    if (rowIndex > 0) {
      salesSheet.getRange(rowIndex, 1, 1, 7).setValues([rowVal]);
    } else {
      salesSheet.appendRow(rowVal);
    }
    return ContentService.createTextOutput(JSON.stringify({status: "success", message: "Sale synced"})).setMimeType(ContentService.MimeType.JSON);
  }

  if (action === "sync_intake") {
    var intakeSheet = sheet.getSheetByName("Intakes") || sheet.insertSheet("Intakes");
    if (intakeSheet.getLastRow() === 0) {
      intakeSheet.appendRow(["เลขที่บันทึก", "เลขที่ใบรับสินค้า/Invoice", "วันที่", "เจ้าหนี้/คู่ค้า", "ยอดเงินรวมสุทธิ (฿)", "ประเภทภาษี (VAT)", "ภาษีมูลค่าเพิ่ม (฿)", "การชำระเงิน"]);
    }
    var intake = requestData.data;
    var data = intakeSheet.getDataRange().getValues();
    var rowIndex = -1;
    for (var iIdx = 1; iIdx < data.length; iIdx++) {
      if (String(data[iIdx][0]) === String(intake.id)) {
        rowIndex = iIdx + 1;
        break;
      }
    }
    var rowVal = [intake.id, intake.invoiceNo, intake.date, intake.creditorName, intake.totalCost, intake.vatType, intake.vatAmount, intake.paymentType];
    if (rowIndex > 0) {
      intakeSheet.getRange(rowIndex, 1, 1, 8).setValues([rowVal]);
    } else {
      intakeSheet.appendRow(rowVal);
    }
    return ContentService.createTextOutput(JSON.stringify({status: "success", message: "Intake synced"})).setMimeType(ContentService.MimeType.JSON);
  }

  if (action === "sync_withdrawal") {
    var wdrSheet = sheet.getSheetByName("Withdrawals") || sheet.insertSheet("Withdrawals");
    if (wdrSheet.getLastRow() === 0) {
      wdrSheet.appendRow(["เลขที่เบิก", "วันที่", "ชื่อสินค้า", "จำนวน", "หน่วยนับ", "เหตุผลการเบิก", "หมายเหตุ"]);
    }
    var wdr = requestData.data;
    var data = wdrSheet.getDataRange().getValues();
    var rowIndex = -1;
    for (var wIdx = 1; wIdx < data.length; wIdx++) {
      if (String(data[wIdx][0]) === String(wdr.id)) {
        rowIndex = wIdx + 1;
        break;
      }
    }
    var rowVal = [wdr.id, wdr.date, wdr.productName, wdr.qty, wdr.unit, wdr.reason, wdr.note];
    if (rowIndex > 0) {
      wdrSheet.getRange(rowIndex, 1, 1, 7).setValues([rowVal]);
    } else {
      wdrSheet.appendRow(rowVal);
    }
    return ContentService.createTextOutput(JSON.stringify({status: "success", message: "Withdrawal synced"})).setMimeType(ContentService.MimeType.JSON);
  }

  if (action === "sync_creditor") {
    var credSheet = sheet.getSheetByName("Creditors") || sheet.insertSheet("Creditors");
    var cred = requestData.data;
    var data = credSheet.getDataRange().getValues();
    var rowIndex = -1;
    for (var i = 1; i < data.length; i++) {
      if (String(data[i][0]) === String(cred.id)) {
        rowIndex = i + 1;
        break;
      }
    }
    if (rowIndex > 0) {
      credSheet.getRange(rowIndex, 1, 1, 4).setValues([[cred.id, cred.name, cred.contact, cred.balance]]);
    } else {
      if (credSheet.getLastRow() === 0) {
        credSheet.appendRow(["รหัสเจ้าหนี้", "ชื่อบริษัท/คู่ค้า", "เบอร์โทรติดต่อ", "ยอดหนี้ค้างชำระ (฿)"]);
      }
      credSheet.appendRow([cred.id, cred.name, cred.contact, cred.balance]);
    }
    return ContentService.createTextOutput(JSON.stringify({status: "success", message: "Creditor synced"})).setMimeType(ContentService.MimeType.JSON);
  }

  if (action === "sync_product") {
    var prodSheet = sheet.getSheetByName("Products") || sheet.insertSheet("Products");
    if (prodSheet.getLastRow() === 0) {
      prodSheet.appendRow(["บาร์โค้ด", "ชื่อสินค้า", "หมวดหมู่", "ราคาทุน", "ราคาปกติ", "ราคาสมาชิก", "สต๊อก", "สต๊อกขั้นต่ำ", "หน่วยนับ"]);
    }
    var prod = requestData.data;
    var data = prodSheet.getDataRange().getValues();
    var rowIndex = -1;
    for (var i = 1; i < data.length; i++) {
      if (String(data[i][0]) === String(prod.barcode)) {
        rowIndex = i + 1;
        break;
      }
    }
    var rowVal = [prod.barcode, prod.name, prod.category, prod.cost, prod.price1, prod.price2, prod.stock, prod.minStock, prod.unit];
    if (rowIndex > 0) {
      prodSheet.getRange(rowIndex, 1, 1, 9).setValues([rowVal]);
    } else {
      prodSheet.appendRow(rowVal);
    }
    return ContentService.createTextOutput(JSON.stringify({status: "success", message: "Product synced"})).setMimeType(ContentService.MimeType.JSON);
  }

  if (action === "sync_member") {
    var memSheet = sheet.getSheetByName("Members") || sheet.insertSheet("Members");
    if (memSheet.getLastRow() === 0) {
      memSheet.appendRow(["รหัสสมาชิก", "ชื่อ-นามสกุล", "เบอร์โทรศัพท์", "ระดับราคา", "คะแนนสะสม"]);
    }
    var mem = requestData.data;
    var data = memSheet.getDataRange().getValues();
    var rowIndex = -1;
    for (var j = 1; j < data.length; j++) {
      if (String(data[j][0]) === String(mem.id)) {
        rowIndex = j + 1;
        break;
      }
    }
    var rowVal = [mem.id, mem.name, mem.phone, mem.priceTier, mem.points];
    if (rowIndex > 0) {
      memSheet.getRange(rowIndex, 1, 1, 5).setValues([rowVal]);
    } else {
      memSheet.appendRow(rowVal);
    }
    return ContentService.createTextOutput(JSON.stringify({status: "success", message: "Member synced"})).setMimeType(ContentService.MimeType.JSON);
  }

  if (action === "sync_promotion") {
    var promoSheet = sheet.getSheetByName("Promotions") || sheet.insertSheet("Promotions");
    if (promoSheet.getLastRow() === 0) {
      promoSheet.appendRow(["รหัสโปรโมชัน", "ชื่อโปรโมชัน", "ประเภท", "เงื่อนไข/ส่วนลด"]);
    }
    var promo = requestData.data;
    var data = promoSheet.getDataRange().getValues();
    var rowIndex = -1;
    for (var k = 1; k < data.length; k++) {
      if (String(data[k][0]) === String(promo.id)) {
        rowIndex = k + 1;
        break;
      }
    }
    var rowVal = [promo.id, promo.name, promo.type, promo.condition || '-'];
    if (rowIndex > 0) {
      promoSheet.getRange(rowIndex, 1, 1, 4).setValues([rowVal]);
    } else {
      promoSheet.appendRow(rowVal);
    }
    return ContentService.createTextOutput(JSON.stringify({status: "success", message: "Promotion synced"})).setMimeType(ContentService.MimeType.JSON);
  }

  if (action === "sync_bulk_fast") {
    var bulk = requestData.data;

    // 1. Sales Bulk Append
    if (bulk.sales && bulk.sales.length > 0) {
      var salesSheet = sheet.getSheetByName("Sales") || sheet.insertSheet("Sales");
      if (salesSheet.getLastRow() === 0) {
        salesSheet.appendRow(["เลขที่บิล", "วันที่", "สมาชิก", "ยอดขายสุทธิ", "ส่วนลด", "กำไรเบื้องต้น", "การชำระเงิน"]);
      }
      var existingSalesData = salesSheet.getDataRange().getValues();
      var existingSaleIds = {};
      for (var s = 1; s < existingSalesData.length; s++) {
        existingSaleIds[String(existingSalesData[s][0])] = true;
      }
      var salesRows = [];
      bulk.sales.forEach(function(sale) {
        if (!existingSaleIds[String(sale.id)]) {
          salesRows.push([sale.id, sale.date, sale.memberName || "ลูกค้าทั่วไป", sale.total, sale.discount, sale.profit, sale.paymentType]);
        }
      });
      if (salesRows.length > 0) {
        salesSheet.getRange(salesSheet.getLastRow() + 1, 1, salesRows.length, 7).setValues(salesRows);
      }
    }

    // 2. Intakes Bulk Append
    if (bulk.intakes && bulk.intakes.length > 0) {
      var intakeSheet = sheet.getSheetByName("Intakes") || sheet.insertSheet("Intakes");
      if (intakeSheet.getLastRow() === 0) {
        intakeSheet.appendRow(["เลขที่บันทึก", "เลขที่ใบรับสินค้า/Invoice", "วันที่", "เจ้าหนี้/คู่ค้า", "ยอดเงินรวมสุทธิ (฿)", "ประเภทภาษี (VAT)", "ภาษีมูลค่าเพิ่ม (฿)", "การชำระเงิน"]);
      }
      var existingIntakesData = intakeSheet.getDataRange().getValues();
      var existingIntakeIds = {};
      for (var k = 1; k < existingIntakesData.length; k++) {
        existingIntakeIds[String(existingIntakesData[k][0])] = true;
      }
      var intakeRows = [];
      bulk.intakes.forEach(function(itk) {
        if (!existingIntakeIds[String(itk.id)]) {
          intakeRows.push([itk.id, itk.invoiceNo, itk.date, itk.creditorName, itk.totalCost, itk.vatType, itk.vatAmount, itk.paymentType]);
        }
      });
      if (intakeRows.length > 0) {
        intakeSheet.getRange(intakeSheet.getLastRow() + 1, 1, intakeRows.length, 8).setValues(intakeRows);
      }
    }

    // 3. Withdrawals Bulk Append
    if (bulk.withdrawals && bulk.withdrawals.length > 0) {
      var wdrSheet = sheet.getSheetByName("Withdrawals") || sheet.insertSheet("Withdrawals");
      if (wdrSheet.getLastRow() === 0) {
        wdrSheet.appendRow(["เลขที่เบิก", "วันที่", "ชื่อสินค้า", "จำนวน", "หน่วยนับ", "เหตุผลการเบิก", "หมายเหตุ"]);
      }
      var existingWdrData = wdrSheet.getDataRange().getValues();
      var existingWdrIds = {};
      for (var w = 1; w < existingWdrData.length; w++) {
        existingWdrIds[String(existingWdrData[w][0])] = true;
      }
      var wdrRows = [];
      bulk.withdrawals.forEach(function(wdr) {
        if (!existingWdrIds[String(wdr.id)]) {
          wdrRows.push([wdr.id, wdr.date, wdr.productName, wdr.qty, wdr.unit, wdr.reason, wdr.note]);
        }
      });
      if (wdrRows.length > 0) {
        wdrSheet.getRange(wdrSheet.getLastRow() + 1, 1, wdrRows.length, 7).setValues(wdrRows);
      }
    }

    // 4. Creditors Bulk Update/Append
    if (bulk.creditors && bulk.creditors.length > 0) {
      var credSheet = sheet.getSheetByName("Creditors") || sheet.insertSheet("Creditors");
      if (credSheet.getLastRow() === 0) {
        credSheet.appendRow(["รหัสเจ้าหนี้", "ชื่อบริษัท/คู่ค้า", "เบอร์โทรติดต่อ", "ยอดหนี้ค้างชำระ (฿)"]);
      }
      var existingCredsData = credSheet.getDataRange().getValues();
      var credRowMap = {};
      for (var c = 1; c < existingCredsData.length; c++) {
        credRowMap[String(existingCredsData[c][0])] = c + 1;
      }
      var newCredRows = [];
      bulk.creditors.forEach(function(cred) {
        var rIdx = credRowMap[String(cred.id)];
        if (rIdx) {
          credSheet.getRange(rIdx, 1, 1, 4).setValues([[cred.id, cred.name, cred.contact, cred.balance]]);
        } else {
          newCredRows.push([cred.id, cred.name, cred.contact, cred.balance]);
        }
      });
      if (newCredRows.length > 0) {
        credSheet.getRange(credSheet.getLastRow() + 1, 1, newCredRows.length, 4).setValues(newCredRows);
      }
    }

    // 5. Products Bulk Update/Append
    if (bulk.products && bulk.products.length > 0) {
      var prodSheet = sheet.getSheetByName("Products") || sheet.insertSheet("Products");
      if (prodSheet.getLastRow() === 0) {
        prodSheet.appendRow(["บาร์โค้ด", "ชื่อสินค้า", "หมวดหมู่", "ราคาทุน", "ราคาปกติ", "ราคาสมาชิก", "สต๊อก", "สต๊อกขั้นต่ำ", "หน่วยนับ"]);
      }
      var existingProdData = prodSheet.getDataRange().getValues();
      var prodRowMap = {};
      for (var p = 1; p < existingProdData.length; p++) {
        prodRowMap[String(existingProdData[p][0])] = p + 1;
      }
      var newProdRows = [];
      bulk.products.forEach(function(prod) {
        var rIdx = prodRowMap[String(prod.barcode)];
        var rowVal = [prod.barcode, prod.name, prod.category, prod.cost, prod.price1, prod.price2, prod.stock, prod.minStock, prod.unit];
        if (rIdx) {
          prodSheet.getRange(rIdx, 1, 1, 9).setValues([rowVal]);
        } else {
          newProdRows.push(rowVal);
        }
      });
      if (newProdRows.length > 0) {
        prodSheet.getRange(prodSheet.getLastRow() + 1, 1, newProdRows.length, 9).setValues(newProdRows);
      }
    }

    // 6. Members Bulk Update/Append
    if (bulk.members && bulk.members.length > 0) {
      var memSheet = sheet.getSheetByName("Members") || sheet.insertSheet("Members");
      if (memSheet.getLastRow() === 0) {
        memSheet.appendRow(["รหัสสมาชิก", "ชื่อ-นามสกุล", "เบอร์โทรศัพท์", "ระดับราคา", "คะแนนสะสม"]);
      }
      var existingMemData = memSheet.getDataRange().getValues();
      var memRowMap = {};
      for (var m = 1; m < existingMemData.length; m++) {
        memRowMap[String(existingMemData[m][0])] = m + 1;
      }
      var newMemRows = [];
      bulk.members.forEach(function(mem) {
        var rIdx = memRowMap[String(mem.id)];
        var rowVal = [mem.id, mem.name, mem.phone, mem.priceTier, mem.points];
        if (rIdx) {
          memSheet.getRange(rIdx, 1, 1, 5).setValues([rowVal]);
        } else {
          newMemRows.push(rowVal);
        }
      });
      if (newMemRows.length > 0) {
        memSheet.getRange(memSheet.getLastRow() + 1, 1, newMemRows.length, 5).setValues(newMemRows);
      }
    }

    // 7. Promotions Bulk Update/Append
    if (bulk.promotions && bulk.promotions.length > 0) {
      var promoSheet = sheet.getSheetByName("Promotions") || sheet.insertSheet("Promotions");
      if (promoSheet.getLastRow() === 0) {
        promoSheet.appendRow(["รหัสโปรโมชัน", "ชื่อโปรโมชัน", "ประเภท", "เงื่อนไข/ส่วนลด"]);
      }
      var existingPromoData = promoSheet.getDataRange().getValues();
      var promoRowMap = {};
      for (var pr = 1; pr < existingPromoData.length; pr++) {
        promoRowMap[String(existingPromoData[pr][0])] = pr + 1;
      }
      var newPromoRows = [];
      bulk.promotions.forEach(function(promo) {
        var rIdx = promoRowMap[String(promo.id)];
        var rowVal = [promo.id, promo.name, promo.type, promo.condition || '-'];
        if (rIdx) {
          promoSheet.getRange(rIdx, 1, 1, 4).setValues([rowVal]);
        } else {
          newPromoRows.push(rowVal);
        }
      });
      if (newPromoRows.length > 0) {
        promoSheet.getRange(promoSheet.getLastRow() + 1, 1, newPromoRows.length, 4).setValues(newPromoRows);
      }
    }

    return ContentService.createTextOutput(JSON.stringify({status: "success", message: "Fast bulk sync completed"})).setMimeType(ContentService.MimeType.JSON);
  }
  
  if (action === "get_products_members") {
    var prodSheet = sheet.getSheetByName("Products");
    var products = [];
    if (prodSheet) {
      var prodData = prodSheet.getDataRange().getValues();
      for (var i = 1; i < prodData.length; i++) {
        products.push({
          barcode: String(prodData[i][0]),
          name: prodData[i][1],
          category: prodData[i][2],
          cost: Number(prodData[i][3]),
          price1: Number(prodData[i][4]),
          price2: Number(prodData[i][5]),
          stock: Number(prodData[i][6]),
          minStock: Number(prodData[i][7]),
          unit: prodData[i][8]
        });
      }
    }
    
    var memSheet = sheet.getSheetByName("Members");
    var members = [];
    if (memSheet) {
      var memData = memSheet.getDataRange().getValues();
      for (var j = 1; j < memData.length; j++) {
        members.push({
          id: String(memData[j][0]),
          name: memData[j][1],
          phone: String(memData[j][2]),
          priceTier: memData[j][3],
          points: Number(memData[j][4])
        });
      }
    }
    
    return ContentService.createTextOutput(JSON.stringify({status: "success", products: products, members: members})).setMimeType(ContentService.MimeType.JSON);
  }
}
                    
  1. กดปุ่มบันทึกโครงการ และกดที่ปุ่ม **การทำให้ใช้งานได้ (Deploy)** > **การนำไปใช้งานได้ใหม่ (New deployment)**
  2. เลือกประเภทเป็น **เว็บแอป (Web app)**
  3. ตั้งค่า **ผู้มีสิทธิ์เข้าถึง (Who has access)** เป็น **ทุกคน (Anyone)** (สำคัญมาก!)
  4. กดปุ่ม **การทำให้ใช้งานได้ (Deploy)** และคัดลอกลิงก์ **URL ของเว็บแอป** มากรอกใส่ช่องตั้งค่าด้านบน