ทำครั้งเดียวต่อเครื่อง — ข้อมูลลูกค้า/ใบรับประกันทั้งหมดจะถูกเก็บไว้ที่ Google Sheet เดียวกัน (Customer_data) ทุกเครื่องที่ตั้งค่า URL นี้จะเห็นข้อมูลชุดเดียวกัน และบันทึกโดยไม่ทับซ้อนกัน
/* ══════════════════════════════════════════════════════════════════════════
B2H SOLAR — Backend สำหรับ "ใบรับประกันสินค้า" (WARRANTY_PRODUCT)
เก็บข้อมูลลูกค้า/ใบรับประกันทั้งหมดไว้ที่ Google Sheet ฉบับเดียว ใช้ร่วมกันทุกเครื่อง
ป้องกันข้อมูลชนกัน/ทับกันด้วย LockService (ล็อกทุกครั้งที่เขียนข้อมูล) และตรวจจับการแก้ไข
ซ้อนกัน (ถ้าคนอื่นบันทึกทับไปก่อนจะไม่เขียนทับเงียบ ๆ)
วิธีติดตั้ง:
1. เปิด https://script.google.com แล้วสร้างโปรเจกต์ใหม่ (New project)
2. ลบโค้ดเดิมออกให้หมด แล้ววางไฟล์นี้ทั้งหมดแทน
3. กด Deploy → New deployment → เลือก Type: Web app
- Execute as: Me
- Who has access: Anyone
4. กด Deploy → คัดลอก Web app URL
5. นำ URL ไปวางที่ WEBAPP_URL ในไฟล์ WARRANTY_PRODUCT.html (ปุ่ม "🗄 ตั้งค่าฐานข้อมูลกลาง")
6. เปิดสเปรดชีตปลายทาง (ตาม SPREADSHEET_ID ด้านล่าง) → เมนู Extensions → Apps Script ครั้งแรก
ที่รัน จะสร้างแท็บ "Customer_data" และ "WarrantySeq" ให้เองอัตโนมัติถ้ายังไม่มี
══════════════════════════════════════════════════════════════════════════ */
const SPREADSHEET_ID = '1-wjvux5w2-gGRMq4WEPPfnKfBUpTcK4DdI3cH197778'; // สเปรดชีตกลางที่เก็บข้อมูลลูกค้า/ใบรับประกันทั้งหมด
const ACCESS_TOKEN = 'B2HWARRANTY2026'; // ← เปลี่ยนเป็นรหัสลับของบริษัทเอง ต้องตรงกับค่าในไฟล์ WARRANTY_PRODUCT.html เป๊ะ ๆ
const CUSTOMER_SHEET_NAME = 'Customer_data';
const WARRANTY_SEQ_SHEET_NAME = 'WarrantySeq'; // แท็บช่วยเก็บเลข RUN ใบรับประกันแยกตามวันที่ (กันเลขซ้ำข้ามเครื่อง)
// คอลัมน์ของแท็บ Customer_data — jsonEquipment เก็บรายการอุปกรณ์ทั้งหมดของใบรับประกันนั้นเป็น JSON ข้อความเดียว
const CUSTOMER_HEADERS = [
'id', 'warrantyNo', 'warrantyNoIsOverride',
'name', 'tel', 'email', 'address',
'date', 'equipmentJson',
'recordedBy', 'recordedByEmail', 'updatedBy', 'updatedByEmail',
'createdAt', 'updatedAt',
'warrantyType', // [เพิ่มใหม่] 'install' (ประกันงานติดตั้ง) หรือ 'product' (ประกันสินค้า)
'cleaningCount', 'cleaningFee', // [เพิ่มใหม่] บริการล้างแผง: จำนวนครั้ง / ค่าบริการ (บาท)
'site', // [เพิ่มใหม่] ชื่อ Site
'mapLink' // [เพิ่มใหม่] ลิงก์ Google Maps — ต่อท้ายเสมอ ไม่แทรกกลาง
// [แก้ไข] เอาคอลัมน์ province/provinceOther/district/districtOther/subdistrict/subdistrictOther/postal ออก
// ให้ตรงกับโครงสร้างชีต Customer_data ที่แก้ไขแล้ว (ฟอร์มฝั่งหน้าเว็บก็เอาช่องเหล่านี้ออกไปแล้วเช่นกัน)
];
const WARRANTY_SEQ_HEADERS = ['date', 'seq'];
function getSS_(){
return SpreadsheetApp.openById(SPREADSHEET_ID);
}
function getCustomerSheet_(){
const ss = getSS_();
let sh = ss.getSheetByName(CUSTOMER_SHEET_NAME);
if(!sh){
sh = ss.insertSheet(CUSTOMER_SHEET_NAME);
sh.appendRow(CUSTOMER_HEADERS);
sh.setFrozenRows(1);
}
// เผื่อชีตเก่าที่มีคอลัมน์ไม่ครบ (เช่นเพิ่มฟีเจอร์ใหม่ทีหลัง) — เติมหัวตารางให้ครบอัตโนมัติ
if(sh.getLastRow() >= 1){
const currentHeaders = sh.getRange(1, 1, 1, CUSTOMER_HEADERS.length).getValues()[0];
if(currentHeaders.length < CUSTOMER_HEADERS.length || currentHeaders[currentHeaders.length - 1] !== CUSTOMER_HEADERS[currentHeaders.length - 1]){
sh.getRange(1, 1, 1, CUSTOMER_HEADERS.length).setValues([CUSTOMER_HEADERS]);
}
}
// [แก้ไข] บังคับคอลัมน์ "tel" (เบอร์โทร) ให้เป็นข้อความล้วน (Plain text) เสมอ — ถ้าปล่อยเป็น Automatic
// Google Sheets จะเดาว่าค่าที่หน้าตาเหมือนตัวเลข (เช่น "0895564360") เป็นตัวเลขจริง แล้วตัดเลข 0 ข้างหน้าทิ้งอัตโนมัติ
// ทำให้เบอร์โทรผิดทันทีที่บันทึก (0895564360 กลายเป็น 895564360) — ตั้งฟอร์แมตทุกครั้งที่เปิดชีต กันไว้เผื่อมีคนไปแก้ฟอร์แมตคอลัมน์นี้ทีหลัง
const telColIndex = CUSTOMER_HEADERS.indexOf('tel') + 1;
if(telColIndex > 0){
sh.getRange(1, telColIndex, Math.max(sh.getMaxRows(), 1000), 1).setNumberFormat('@');
}
return sh;
}
function getWarrantySeqSheet_(){
const ss = getSS_();
let sh = ss.getSheetByName(WARRANTY_SEQ_SHEET_NAME);
if(!sh){
sh = ss.insertSheet(WARRANTY_SEQ_SHEET_NAME);
sh.appendRow(WARRANTY_SEQ_HEADERS);
sh.setFrozenRows(1);
}
return sh;
}
// [เพิ่มใหม่] งานติดตามหลังการขาย — เก็บแยกแท็บ ใช้ Web App/token เดียวกับใบรับประกัน
const FOLLOWUP_SHEET_NAME = 'FollowUp_data';
const FOLLOWUP_HEADERS = [
'id', 'warrantyNo', 'customerName', 'tel',
'followUpDate', 'note',
'recordedBy', 'recordedByEmail',
'createdAt', 'updatedAt',
'surveyJson' // [เพิ่มใหม่] คำตอบแบบสอบถามความพึงพอใจ (ให้คะแนน 1-10 ข้อ + ปลายเปิด 5 ข้อ) เก็บเป็น JSON ในคอลัมน์เดียว
];
function getFollowUpSheet_(){
const ss = getSS_();
let sh = ss.getSheetByName(FOLLOWUP_SHEET_NAME);
if(!sh){
sh = ss.insertSheet(FOLLOWUP_SHEET_NAME);
sh.appendRow(FOLLOWUP_HEADERS);
sh.setFrozenRows(1);
}
if(sh.getLastRow() >= 1){
const currentHeaders = sh.getRange(1, 1, 1, FOLLOWUP_HEADERS.length).getValues()[0];
if(currentHeaders.length < FOLLOWUP_HEADERS.length || currentHeaders[currentHeaders.length - 1] !== FOLLOWUP_HEADERS[currentHeaders.length - 1]){
sh.getRange(1, 1, 1, FOLLOWUP_HEADERS.length).setValues([FOLLOWUP_HEADERS]);
}
}
return sh;
}
// [เพิ่มใหม่] Service หลังการขาย (แจ้งซ่อม/เคสบริการ) — เก็บแยกแท็บ ใช้ Web App/token เดียวกับใบรับประกัน
const SERVICE_SHEET_NAME = 'Service_data';
const SERVICE_HEADERS = [
'id', 'warrantyNo', 'customerName', 'tel',
'serviceDate', 'issue', 'status', 'note',
'recordedBy', 'recordedByEmail',
'createdAt', 'updatedAt',
'serviceType', // [เพิ่มใหม่] ประเภทงาน: 'repair' (แจ้งซ่อม) / 'inspection' (ตรวจสอบตามระยะ) / 'cleaning' (ล้างแผงตามวาระ)
'assignee', 'assigneeType', // [เพิ่มใหม่] ผู้รับผิดชอบ: ชื่อ + ประเภท 'individual' (รายบุคคล) / 'group' (กลุ่ม/ทีม)
'calendarEventId', // [เพิ่มใหม่] รหัสกิจกรรมใน Google Calendar ของบัญชีที่ Deploy สคริปต์นี้
'attachmentFileId', 'attachmentUrl', 'attachmentName' // [แก้ไข] ไฟล์แนบ (PDF/JPG/PNG) — ต่อท้ายเสมอ ไม่แทรกกลาง
];
function getServiceSheet_(){
const ss = getSS_();
let sh = ss.getSheetByName(SERVICE_SHEET_NAME);
if(!sh){
sh = ss.insertSheet(SERVICE_SHEET_NAME);
sh.appendRow(SERVICE_HEADERS);
sh.setFrozenRows(1);
}
if(sh.getLastRow() >= 1){
const currentHeaders = sh.getRange(1, 1, 1, SERVICE_HEADERS.length).getValues()[0];
if(currentHeaders.length < SERVICE_HEADERS.length || currentHeaders[currentHeaders.length - 1] !== SERVICE_HEADERS[currentHeaders.length - 1]){
sh.getRange(1, 1, 1, SERVICE_HEADERS.length).setValues([SERVICE_HEADERS]);
}
}
// [แก้ไข] บังคับคอลัมน์ "tel" (เบอร์โทร) ให้เป็นข้อความล้วนเสมอ เหมือนที่แก้ไว้ใน getCustomerSheet_() —
// กัน Google Sheets ตัดเลข 0 ข้างหน้าทิ้งอัตโนมัติ (สาเหตุที่เบอร์โทรเพี้ยนตอนส่งต่อไปยังระบบนัดหมายด้วย
// เพราะเคสบริการดึงเบอร์มาจากชีตนี้ ถ้าเบอร์ในนี้เพี้ยนไปแล้ว ส่งต่อไปที่ไหนก็เพี้ยนตามไปด้วย)
const telColIndex = SERVICE_HEADERS.indexOf('tel') + 1;
if(telColIndex > 0){
sh.getRange(1, telColIndex, Math.max(sh.getMaxRows(), 1000), 1).setNumberFormat('@');
}
return sh;
}
// [เพิ่มใหม่] งานขอขนานไฟฟ้า (MEA/PEA) — เก็บแยกแท็บ ใช้ Web App/token เดียวกับใบรับประกัน ต่างจาก Service_data
// ตรงที่เป็น "สถานะล่าสุด" ของลูกค้าแต่ละคน (1 คน = 1 แถว) ไม่ใช่ประวัติสะสม — บันทึกซ้ำของลูกค้าคนเดิม (จับคู่ด้วย
// เลขที่ใบรับประกันก่อน ถ้าไม่มีใช้เบอร์โทรแทน) จะอัปเดตแถวเดิมแทนการสร้างแถวใหม่ (ดู savePowerRequest_ ด้านล่าง)
const POWER_SHEET_NAME = 'PowerRequest_data';
const POWER_HEADERS = [
'id', 'warrantyNo', 'customerName', 'tel', 'address',
'authority', // 'MEA' (การไฟฟ้านครหลวง) / 'PEA' (การไฟฟ้าส่วนภูมิภาค)
'status', // 'no_doc' (ไม่มีเอกสาร) / 'missing_doc' (ขาดเอกสาร) / 'pending_submit' (รอยื่น) / 'submitted' (ยื่นแล้ว) / 'success' (สำเร็จ)
'caseNo', // [เพิ่มใหม่] เลขที่รับเรื่อง — กรอกเฉพาะตอนสถานะเป็น "สำเร็จ"
'note', // [เพิ่มใหม่] หมายเหตุ — กรอกเฉพาะตอนสถานะเป็น "ไม่มีเอกสาร/ขาดเอกสาร/รอยื่น"
'recordedBy', 'recordedByEmail',
'createdAt', 'updatedAt'
];
function getPowerSheet_(){
const ss = getSS_();
let sh = ss.getSheetByName(POWER_SHEET_NAME);
if(!sh){
sh = ss.insertSheet(POWER_SHEET_NAME);
sh.appendRow(POWER_HEADERS);
sh.setFrozenRows(1);
}
if(sh.getLastRow() >= 1){
const currentHeaders = sh.getRange(1, 1, 1, POWER_HEADERS.length).getValues()[0];
if(currentHeaders.length < POWER_HEADERS.length || currentHeaders[currentHeaders.length - 1] !== POWER_HEADERS[currentHeaders.length - 1]){
sh.getRange(1, 1, 1, POWER_HEADERS.length).setValues([POWER_HEADERS]);
}
}
// [แก้ไข] บังคับคอลัมน์ "tel" ให้เป็นข้อความล้วนเสมอ เหมือนที่แก้ไว้ในชีตอื่น ๆ — กัน Google Sheets ตัดเลข 0 ข้างหน้าทิ้ง
const telColIndex = POWER_HEADERS.indexOf('tel') + 1;
if(telColIndex > 0){
sh.getRange(1, telColIndex, Math.max(sh.getMaxRows(), 1000), 1).setNumberFormat('@');
}
return sh;
}
// [เพิ่มใหม่] ไฟล์แนบเคสบริการ (PDF/JPG/PNG) — เก็บใน Drive โฟลเดอร์แยกต่างหาก แล้วแชร์ลิงก์แบบดูได้ทุกคนที่มีลิงก์
const SERVICE_ATTACHMENT_FOLDER_NAME = 'B2H Solar - ไฟล์แนบเคสบริการ';
function getServiceAttachmentFolder_(){
const folders = DriveApp.getFoldersByName(SERVICE_ATTACHMENT_FOLDER_NAME);
if(folders.hasNext()) return folders.next();
return DriveApp.createFolder(SERVICE_ATTACHMENT_FOLDER_NAME);
}
function detectMimeFromDataUrl_(dataUrl){
const m = String(dataUrl || '').match(/^data:([^;]+);base64,/);
return m ? m[1] : '';
}
function saveServiceAttachmentFile_(base64Data, fileName){
const folder = getServiceAttachmentFolder_();
const cleanBase64 = String(base64Data || '').split(',').pop(); // ตัดส่วน "data:...;base64," ออกถ้ามี
const bytes = Utilities.base64Decode(cleanBase64);
const mimeType = detectMimeFromDataUrl_(base64Data) || 'application/pdf';
const blob = Utilities.newBlob(bytes, mimeType, fileName || 'ไฟล์แนบเคสบริการ');
const file = folder.createFile(blob);
file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
return file;
}
function deleteServiceAttachmentFile_(fileId){
if(!fileId) return;
try{ DriveApp.getFileById(fileId).setTrashed(true); }catch(e){ /* ไฟล์อาจถูกลบไปแล้ว หรือไม่มีสิทธิ์ — ไม่ต้องหยุดการทำงาน */ }
}
// [เพิ่มใหม่] เชื่อมกับสเปรดชีต "ตารางนัด" (แยกไฟล์จากฐานข้อมูลใบรับประกัน/Service ด้านบน) เพื่อดึงรายชื่อ
// พนักงาน (แท็บ Staff) และกลุ่มงาน (แท็บ StaffGroups) มาใช้กับปุ่ม "ระบุผู้รับผิดชอบ" ในหน้า Service หลังการขาย
// — บัญชีที่ใช้ Deploy สคริปต์นี้ (Execute as: Me) ต้องมีสิทธิ์เข้าถึงสเปรดชีต "ตารางนัด" ด้วย จึงจะอ่านได้สำเร็จ
const STAFF_SPREADSHEET_ID = '1gPri6c8kyyeuwfMBpQ1st1QcWNNNnwSEFwBu9QbzBio';
const STAFF_SHEET_NAME = 'Staff';
const STAFF_GROUPS_SHEET_NAME = 'StaffGroups';
// อ่านทุกแถวโดยใช้หัวตารางแถวแรกเป็นชื่อคอลัมน์ (ไม่ผูกกับโครงสร้างคอลัมน์ตายตัวเหมือน sheetToObjects_ ด้านล่าง
// เพราะไฟล์ "ตารางนัด" เป็นสเปรดชีตแยกต่างหาก ไม่ได้ควบคุมโครงสร้างคอลัมน์เอง)
function sheetRowsAsObjects_(sh){
const lastRow = sh.getLastRow();
const lastCol = sh.getLastColumn();
if(lastRow < 2 || lastCol < 1) return [];
const headers = sh.getRange(1, 1, 1, lastCol).getValues()[0].map(h => String(h || '').trim().toLowerCase());
const values = sh.getRange(2, 1, lastRow - 1, lastCol).getValues();
return values.map(row => {
const obj = {};
headers.forEach((h, c) => { if(h) obj[h] = row[c]; });
return obj;
}).filter(o => Object.keys(o).some(k => String(o[k] || '').trim())); // ข้ามแถวว่างเปล่าจริง ๆ
}
function getStaffDirectory_(){
const ss = SpreadsheetApp.openById(STAFF_SPREADSHEET_ID);
const staffSh = ss.getSheetByName(STAFF_SHEET_NAME);
const groupsSh = ss.getSheetByName(STAFF_GROUPS_SHEET_NAME);
const staff = staffSh ? sheetRowsAsObjects_(staffSh)
.map(o => ({ id: String(o.id || ''), name: String(o.name || '').trim(), email: String(o.email || '').trim() }))
.filter(o => o.name) : [];
const groups = groupsSh ? sheetRowsAsObjects_(groupsSh)
.map(o => ({ id: String(o.id || ''), name: String(o.name || o.group || o.groupname || '').trim() }))
.filter(o => o.name) : [];
return { staff: staff, groups: groups };
}
function sheetToObjects_(sh, headers){
const lastRow = sh.getLastRow();
if(lastRow < 2) return [];
const values = sh.getRange(2, 1, lastRow - 1, headers.length).getValues();
return values.map((row, i) => {
const obj = { _row: i + 2 };
headers.forEach((h, c) => { obj[h] = row[c]; });
return obj;
}).filter(o => o.id || o.date); // ข้ามแถวว่างเปล่าจริง ๆ
}
/* ══════════════════════════════════════════════ จุดเข้า Web App ══════════════════════════════════════════════ */
function doGet(e){
try{
const action = (e.parameter.action || '').trim();
if(!checkToken_(e.parameter.token)) return jsonOut_({ ok:false, error:'invalid token' });
if(action === 'bootstrap'){
const entries = sheetToObjects_(getCustomerSheet_(), CUSTOMER_HEADERS).map(entryForClient_);
return jsonOut_({ ok:true, entries: entries });
}
if(action === 'peekNextWarrantySeq'){
// อ่านอย่างเดียว ไม่ล็อก ไม่เพิ่มเลข — ใช้แสดงตัวอย่างเลขที่ใบรับประกันบนหน้าจอเฉย ๆ ก่อนกดพิมพ์จริง
const dateStr = String(e.parameter.date || '').trim();
const seq = peekSeqForDate_(dateStr);
return jsonOut_({ ok:true, nextSeq: seq });
}
if(action === 'followUpBootstrap'){ // [เพิ่มใหม่] งานติดตามหลังการขาย
const entries = sheetToObjects_(getFollowUpSheet_(), FOLLOWUP_HEADERS).map(followUpEntryForClient_);
return jsonOut_({ ok:true, entries: entries });
}
if(action === 'serviceBootstrap'){ // [เพิ่มใหม่] Service หลังการขาย
const entries = sheetToObjects_(getServiceSheet_(), SERVICE_HEADERS).map(serviceEntryForClient_);
return jsonOut_({ ok:true, entries: entries });
}
if(action === 'powerRequestBootstrap'){ // [เพิ่มใหม่] งานขอขนานไฟฟ้า
const entries = sheetToObjects_(getPowerSheet_(), POWER_HEADERS).map(powerEntryForClient_);
return jsonOut_({ ok:true, entries: entries });
}
if(action === 'staffBootstrap'){ // [เพิ่มใหม่] รายชื่อพนักงาน/กลุ่มงาน จากไฟล์ "ตารางนัด" — ใช้กับปุ่ม "ระบุผู้รับผิดชอบ"
try{
const dir = getStaffDirectory_();
return jsonOut_({ ok:true, staff: dir.staff, groups: dir.groups });
}catch(dirErr){
return jsonOut_({ ok:false, error:'เชื่อมต่อไฟล์ตารางนัดไม่สำเร็จ: ' + String(dirErr) });
}
}
return jsonOut_({ ok:false, error:'unknown action' });
}catch(err){
return jsonOut_({ ok:false, error:String(err) });
}
}
function doPost(e){
try{
const body = JSON.parse(e.postData.contents || '{}');
if(!checkToken_(body.token)) return jsonOut_({ ok:false, error:'invalid token' });
const action = body.action;
if(action === 'saveEntry') return jsonOut_(withLock_(() => saveEntry_(body.entry)));
if(action === 'deleteEntry') return jsonOut_(withLock_(() => deleteEntry_(body.id)));
if(action === 'reserveWarrantySeq') return jsonOut_(withLock_(() => reserveSeqForDate_(body.date)));
if(action === 'saveFollowUp') return jsonOut_(withLock_(() => saveFollowUp_(body.entry))); // [เพิ่มใหม่]
if(action === 'deleteFollowUp') return jsonOut_(withLock_(() => deleteFollowUp_(body.id))); // [เพิ่มใหม่]
if(action === 'saveService') return jsonOut_(withLock_(() => saveService_(body.entry))); // [เพิ่มใหม่]
if(action === 'deleteService') return jsonOut_(withLock_(() => deleteService_(body.id))); // [เพิ่มใหม่]
if(action === 'savePowerRequest') return jsonOut_(withLock_(() => savePowerRequest_(body.entry))); // [เพิ่มใหม่]
if(action === 'deletePowerRequest') return jsonOut_(withLock_(() => deletePowerRequest_(body.id))); // [เพิ่มใหม่]
return jsonOut_({ ok:false, error:'unknown action' });
}catch(err){
return jsonOut_({ ok:false, error:String(err) });
}
}
function checkToken_(token){ return token === ACCESS_TOKEN; }
function jsonOut_(obj){
return ContentService.createTextOutput(JSON.stringify(obj)).setMimeType(ContentService.MimeType.JSON);
}
// ทุกการเขียนข้อมูล (save/delete/reserve เลขที่) ต้องได้ล็อกสคริปต์ก่อนเสมอ กันสองเครื่องเขียน
// พร้อมกันแล้วข้อมูลชนกัน/ทับกัน (race condition)
function withLock_(fn){
const lock = LockService.getScriptLock();
try{
lock.waitLock(30000);
}catch(e){
return { ok:false, error:'ระบบกำลังมีคนอื่นบันทึกข้อมูลอยู่ กรุณาลองอีกครั้งใน 2-3 วินาที' };
}
try{
return fn();
} finally {
lock.releaseLock();
}
}
function entryForClient_(o){
const c = Object.assign({}, o);
delete c._row;
let equipment = [];
try{ equipment = JSON.parse(c.equipmentJson || '[]'); }catch(e){ equipment = []; }
delete c.equipmentJson;
c.equipment = equipment;
c.updatedAt = c.updatedAt instanceof Date ? c.updatedAt.toISOString() : String(c.updatedAt || '');
c.createdAt = c.createdAt instanceof Date ? c.createdAt.toISOString() : String(c.createdAt || '');
return c;
}
/* ══════════════════════════════════════════════ บันทึก/ลบ ใบรับประกัน ══════════════════════════════════════════════ */
function saveEntry_(entry){
if(!entry) return { ok:false, error:'ไม่มีข้อมูลที่จะบันทึก' };
const sh = getCustomerSheet_();
const list = sheetToObjects_(sh, CUSTOMER_HEADERS);
let id = String(entry.id || '').trim();
const name = String((entry.info && entry.info.name) || '').trim();
if(!name) return { ok:false, error:'กรุณากรอกชื่อ-นามสกุลลูกค้าก่อนบันทึก' };
let existing = null;
if(id) existing = list.find(x => x.id === id);
// ป้องกันข้อมูลทับกันเวลามีหลายเครื่อง/หลายคนแก้ไขใบรับประกันเดียวกันพร้อมกัน: ฝั่งเว็บจะแนบ
// เวลา updatedAt ล่าสุดที่ตัวเองเคยเห็นมาด้วย ถ้าไม่ตรงกับค่าล่าสุดในชีต (มีคนอื่นบันทึกทับไปก่อน
// แล้วระหว่างที่เรากำลังแก้อยู่) ให้ปฏิเสธการบันทึกแทนที่จะเขียนทับเงียบ ๆ
if(existing){
const existingUpdatedAtIso = existing.updatedAt instanceof Date ? existing.updatedAt.toISOString() : String(existing.updatedAt || '');
const clientKnownUpdatedAt = String(entry.clientUpdatedAt || '');
if(clientKnownUpdatedAt && existingUpdatedAtIso && clientKnownUpdatedAt !== existingUpdatedAtIso){
return {
ok:false,
conflict:true,
error:'ใบรับประกันนี้ถูกแก้ไขโดย ' + (existing.updatedBy || existing.recordedBy || 'ผู้ใช้อื่น') + ' ไปแล้วระหว่างที่คุณกำลังแก้ไข กรุณาโหลดข้อมูลล่าสุดก่อนบันทึกซ้ำ',
latestEntry: entryForClient_(existing)
};
}
}
if(!id) id = Utilities.getUuid();
const recordedByName = String(entry.recordedByName || '').trim() || 'ไม่ระบุชื่อ';
const recordedByEmail = String(entry.recordedByEmail || '').trim();
const recordedBy = existing && existing.recordedBy ? existing.recordedBy : recordedByName;
const recordedByEmailFinal = existing && existing.recordedByEmail ? existing.recordedByEmail : recordedByEmail;
const createdAt = existing ? existing.createdAt : new Date();
const now = new Date();
const info = entry.info || {};
// [เพิ่มใหม่] ประเภทการรับประกัน: 'install' (งานติดตั้ง) หรือ 'product' (สินค้า) — ค่าเริ่มต้น 'install'
const allowedWarrantyTypes = ['install', 'product'];
const warrantyType = allowedWarrantyTypes.indexOf(info.warrantytype) !== -1 ? info.warrantytype : 'install';
// [เพิ่มใหม่] บริการล้างแผง — เก็บเฉพาะตอนเป็นงานติดตั้งเท่านั้น (ประกันสินค้าไม่มีบริการนี้)
const cleaningCount = (warrantyType === 'install' && info.cleaningcount !== undefined && info.cleaningcount !== '') ? Number(info.cleaningcount) || 0 : '';
const cleaningFee = (warrantyType === 'install' && info.cleaningfee !== undefined && info.cleaningfee !== '') ? Number(info.cleaningfee) || 0 : '';
const row = [
id,
String(entry.warrantyNo || '').trim(),
entry.warrantyNoIsOverride === true,
name,
String(info.tel || '').trim(),
String(info.email || '').trim(),
String(info.address || '').trim(),
String(info.date || '').trim(),
JSON.stringify(Array.isArray(entry.equipment) ? entry.equipment : []),
recordedBy, recordedByEmailFinal,
recordedByName, recordedByEmail,
createdAt, now,
warrantyType, // [เพิ่มใหม่]
cleaningCount, cleaningFee, // [เพิ่มใหม่]
String(info.site || '').trim(), // [เพิ่มใหม่] ชื่อ Site
String(info.maplink || '').trim() // [เพิ่มใหม่] ลิงก์ Google Maps — ต้องอยู่ตำแหน่งสุดท้าย ให้ตรงกับ CUSTOMER_HEADERS
];
if(existing){
sh.getRange(existing._row, 1, 1, CUSTOMER_HEADERS.length).setValues([row]);
} else {
sh.appendRow(row);
}
const saved = sheetToObjects_(sh, CUSTOMER_HEADERS).find(x => x.id === id);
return { ok:true, entry: entryForClient_(saved) };
}
function deleteEntry_(id){
if(!id) return { ok:false, error:'ไม่พบรหัสรายการ' };
const sh = getCustomerSheet_();
const list = sheetToObjects_(sh, CUSTOMER_HEADERS);
const found = list.find(x => x.id === id);
if(!found) return { ok:false, error:'ไม่พบรายการนี้ (อาจถูกลบไปแล้วจากเครื่องอื่น)' };
sh.deleteRow(found._row);
return { ok:true };
}
/* ══════════════════════════════════════════════ [เพิ่มใหม่] งานติดตามหลังการขาย (FollowUp_data) ══════════════════════════════════════════════ */
function followUpEntryForClient_(o){
const c = Object.assign({}, o);
delete c._row;
let survey = null;
try{ survey = c.surveyJson ? JSON.parse(c.surveyJson) : null; }catch(e){ survey = null; } // [เพิ่มใหม่]
delete c.surveyJson;
c.survey = survey;
// [แก้ไข] คอลัมน์ followUpDate ในชีตถูกจัดรูปแบบอัตโนมัติเป็นวันที่ ทำให้ Apps Script อ่านค่ากลับมาเป็น
// Date object เต็มรูปแบบแทนที่จะเป็นสตริง "YYYY-MM-DD" เฉยๆ ถ้าแปลงด้วย toISOString() ตรงๆ (ซึ่งอิง UTC)
// จะได้วันที่เพี้ยนย้อนไป 1 วันเมื่อเทียบเวลาไทย (เช่น 19 ก.ค. กลายเป็น "18T17:00:00.000Z") ต้องแปลงด้วย
// เขตเวลาของสคริปต์ (ตั้งเป็น Asia/Bangkok ได้ที่ Project Settings) ให้ได้วันที่ถูกต้องตรงกับที่กรอกจริง
c.followUpDate = c.followUpDate instanceof Date
? Utilities.formatDate(c.followUpDate, Session.getScriptTimeZone() || 'Asia/Bangkok', 'yyyy-MM-dd')
: String(c.followUpDate || '');
c.updatedAt = c.updatedAt instanceof Date ? c.updatedAt.toISOString() : String(c.updatedAt || '');
c.createdAt = c.createdAt instanceof Date ? c.createdAt.toISOString() : String(c.createdAt || '');
return c;
}
function saveFollowUp_(entry){
if(!entry) return { ok:false, error:'ไม่มีข้อมูลที่จะบันทึก' };
const sh = getFollowUpSheet_();
const list = sheetToObjects_(sh, FOLLOWUP_HEADERS);
let id = String(entry.id || '').trim();
const customerName = String(entry.customerName || '').trim();
if(!customerName) return { ok:false, error:'กรุณากรอกชื่อลูกค้าก่อนบันทึก' };
const note = String(entry.note || '').trim();
if(!note) return { ok:false, error:'กรุณากรอกรายละเอียดการติดตามก่อนบันทึก' };
let existing = null;
if(id) existing = list.find(x => x.id === id);
if(!id) id = Utilities.getUuid();
const recordedByName = String(entry.recordedByName || '').trim() || 'ไม่ระบุชื่อ';
const recordedByEmail = String(entry.recordedByEmail || '').trim();
const createdAt = existing ? existing.createdAt : new Date();
const now = new Date();
const row = [
id,
String(entry.warrantyNo || '').trim(),
customerName,
String(entry.tel || '').trim(),
String(entry.followUpDate || '').trim(),
note,
recordedByName, recordedByEmail,
createdAt, now,
String(entry.surveyJson || '') // [เพิ่มใหม่] ต้องอยู่ตำแหน่งสุดท้าย ให้ตรงกับ FOLLOWUP_HEADERS
];
if(existing){
sh.getRange(existing._row, 1, 1, FOLLOWUP_HEADERS.length).setValues([row]);
} else {
sh.appendRow(row);
}
const saved = sheetToObjects_(sh, FOLLOWUP_HEADERS).find(x => x.id === id);
return { ok:true, entry: followUpEntryForClient_(saved) };
}
function deleteFollowUp_(id){
if(!id) return { ok:false, error:'ไม่พบรหัสรายการ' };
const sh = getFollowUpSheet_();
const list = sheetToObjects_(sh, FOLLOWUP_HEADERS);
const found = list.find(x => x.id === id);
if(!found) return { ok:false, error:'ไม่พบรายการนี้ (อาจถูกลบไปแล้วจากเครื่องอื่น)' };
sh.deleteRow(found._row);
return { ok:true };
}
/* ══════════════════════════════════════════════ [เพิ่มใหม่] Service หลังการขาย (Service_data) ══════════════════════════════════════════════ */
function serviceEntryForClient_(o){
const c = Object.assign({}, o);
delete c._row;
// [แก้ไข] เช่นเดียวกับ followUpDate — คอลัมน์ serviceDate อาจถูกจัดรูปแบบอัตโนมัติเป็นวันที่ ต้องแปลงด้วย
// เขตเวลาของสคริปต์ (Asia/Bangkok) กันวันที่เพี้ยนย้อนไป 1 วัน
c.serviceDate = c.serviceDate instanceof Date
? Utilities.formatDate(c.serviceDate, Session.getScriptTimeZone() || 'Asia/Bangkok', 'yyyy-MM-dd')
: String(c.serviceDate || '');
c.updatedAt = c.updatedAt instanceof Date ? c.updatedAt.toISOString() : String(c.updatedAt || '');
c.createdAt = c.createdAt instanceof Date ? c.createdAt.toISOString() : String(c.createdAt || '');
return c;
}
// [เพิ่มใหม่] ซิงก์เคสบริการขึ้น Google Calendar ของบัญชีที่ Deploy สคริปต์นี้ (ปฏิทินเริ่มต้น) — สร้างเป็นกิจกรรม
// เต็มวันตามวันที่แจ้งซ่อม ถ้าเคสนี้เคยมีกิจกรรมอยู่แล้ว (existingEventId) จะอัปเดตกิจกรรมเดิมแทนการสร้างซ้ำ
// ถ้าซิงก์ไม่สำเร็จ (เช่น ยังไม่ได้อนุญาตสิทธิ์ปฏิทินตอน Deploy) จะไม่ทำให้การบันทึกเคสบริการหลักล้มเหลว
function upsertServiceCalendarEvent_(entry, existingEventId){
try{
const dateStr = String(entry.serviceDate || '').trim();
if(!dateStr) return { id: existingEventId || '', ok: false };
const parts = dateStr.split('-');
if(parts.length !== 3) return { id: existingEventId || '', ok: false };
const eventDate = new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
const typeLabelMap = { repair: '🔧 แจ้งซ่อม', inspection: '🔍 ตรวจสอบตามระยะ', cleaning: '🧹 ล้างแผงตามวาระ' };
const typeLabel = typeLabelMap[entry.serviceType] || typeLabelMap.repair;
const title = typeLabel + ' - ' + (entry.customerName || 'ไม่ระบุชื่อ');
const assigneeLine = entry.assignee
? (entry.assigneeType === 'group' ? 'กลุ่ม: ' : '') + entry.assignee
: 'ยังไม่ระบุ';
const statusLabelMap = { pending: 'รอนัด', inprogress: 'กำลังดำเนินการ', done: 'เสร็จสิ้น', sent: 'ส่งเข้าระบบนัดหมายแล้ว' };
const descLines = [
'ลูกค้า: ' + (entry.customerName || '-'),
'เบอร์โทร: ' + (entry.tel || '-'),
'เลขที่ใบรับประกัน: ' + (entry.warrantyNo || '-'),
'อาการ/ปัญหาที่แจ้ง: ' + (entry.issue || '-'),
'ผู้รับผิดชอบ: ' + assigneeLine,
'สถานะ: ' + (statusLabelMap[entry.status] || entry.status || '-'),
entry.note ? ('หมายเหตุ: ' + entry.note) : ''
].filter(line => !!line);
const description = descLines.join('\n');
const cal = CalendarApp.getDefaultCalendar();
let event = null;
if(existingEventId){
try{ event = cal.getEventById(existingEventId); }catch(getErr){ event = null; }
}
if(event){
event.setTitle(title);
event.setDescription(description);
event.setAllDayDate(eventDate);
return { id: event.getId(), ok: true };
}
const newEvent = cal.createAllDayEvent(title, eventDate, { description: description });
return { id: newEvent.getId(), ok: true };
}catch(err){
return { id: existingEventId || '', ok: false };
}
}
function saveService_(entry){
if(!entry) return { ok:false, error:'ไม่มีข้อมูลที่จะบันทึก' };
const sh = getServiceSheet_();
const list = sheetToObjects_(sh, SERVICE_HEADERS);
let id = String(entry.id || '').trim();
const customerName = String(entry.customerName || '').trim();
if(!customerName) return { ok:false, error:'กรุณากรอกชื่อลูกค้าก่อนบันทึก' };
const issue = String(entry.issue || '').trim(); // [แก้ไข] ไม่บังคับกรอกแล้ว
let existing = null;
if(id) existing = list.find(x => x.id === id);
if(!id) id = Utilities.getUuid();
const recordedByName = String(entry.recordedByName || '').trim() || 'ไม่ระบุชื่อ';
const recordedByEmail = String(entry.recordedByEmail || '').trim();
const createdAt = existing ? existing.createdAt : new Date();
const now = new Date();
const allowedStatuses = ['pending', 'inprogress', 'done', 'sent']; // [เพิ่มใหม่] 'sent' = ส่งเข้าระบบนัดหมายแล้ว
const status = allowedStatuses.indexOf(entry.status) !== -1 ? entry.status : 'pending';
const allowedServiceTypes = ['repair', 'inspection', 'cleaning']; // [เพิ่มใหม่] แจ้งซ่อม / ตรวจสอบตามระยะ / ล้างแผงตามวาระ
const serviceType = allowedServiceTypes.indexOf(entry.serviceType) !== -1 ? entry.serviceType : 'repair';
const allowedAssigneeTypes = ['individual', 'group']; // [เพิ่มใหม่] ผู้รับผิดชอบ: รายบุคคล / กลุ่ม
const assigneeType = allowedAssigneeTypes.indexOf(entry.assigneeType) !== -1 ? entry.assigneeType : 'individual';
const assignee = String(entry.assignee || '').trim();
const serviceDate = String(entry.serviceDate || '').trim();
const tel = String(entry.tel || '').trim();
const warrantyNo = String(entry.warrantyNo || '').trim();
const note = String(entry.note || '').trim();
// [แก้ไข] ไฟล์แนบ (PDF/JPG/PNG) — ถ้าไม่ได้เลือกไฟล์ใหม่/ไม่ได้สั่งลบ ให้คงไฟล์เดิมไว้เฉย ๆ (ถ้ามี)
let attachmentFileId = existing ? String(existing.attachmentFileId || '').trim() : '';
let attachmentUrl = existing ? String(existing.attachmentUrl || '').trim() : '';
let attachmentName = existing ? String(existing.attachmentName || '').trim() : '';
try{
if(entry.removeAttachment === true && attachmentFileId){
deleteServiceAttachmentFile_(attachmentFileId);
attachmentFileId = ''; attachmentUrl = ''; attachmentName = '';
} else if(entry.attachmentBase64){
if(attachmentFileId) deleteServiceAttachmentFile_(attachmentFileId); // มีไฟล์เก่าอยู่แล้ว ให้ลบทิ้งก่อนแทนที่ด้วยไฟล์ใหม่
const fileName = String(entry.attachmentName || 'ไฟล์แนบเคสบริการ').trim();
const file = saveServiceAttachmentFile_(entry.attachmentBase64, fileName);
attachmentFileId = file.getId();
attachmentUrl = file.getUrl();
attachmentName = fileName;
}
}catch(err){
// อัปโหลดไฟล์ไม่สำเร็จ ไม่ควรทำให้การบันทึกเคสบริการทั้งหมดล้มเหลวไปด้วย — แค่ไม่มีไฟล์แนบใหม่
}
// [เพิ่มใหม่] ซิงก์ขึ้น Google Calendar — สร้าง/อัปเดตกิจกรรมตามวันที่แจ้งซ่อมทุกครั้งที่บันทึก
const existingEventId = existing ? String(existing.calendarEventId || '') : '';
const calResult = upsertServiceCalendarEvent_({
customerName: customerName, tel: tel, warrantyNo: warrantyNo, serviceDate: serviceDate,
issue: issue, status: status, note: note, serviceType: serviceType, assignee: assignee, assigneeType: assigneeType
}, existingEventId);
const row = [
id,
warrantyNo,
customerName,
tel,
serviceDate,
issue,
status,
note,
recordedByName, recordedByEmail,
createdAt, now,
serviceType,
assignee, assigneeType,
calResult.id,
attachmentFileId, attachmentUrl, attachmentName // [แก้ไข] ต้องอยู่ตำแหน่งสุดท้าย ให้ตรงกับ SERVICE_HEADERS
];
if(existing){
sh.getRange(existing._row, 1, 1, SERVICE_HEADERS.length).setValues([row]);
} else {
sh.appendRow(row);
}
const saved = sheetToObjects_(sh, SERVICE_HEADERS).find(x => x.id === id);
const clientEntry = serviceEntryForClient_(saved);
clientEntry.calendarSynced = calResult.ok; // [เพิ่มใหม่] แจ้งฝั่งหน้าเว็บว่าซิงก์ปฏิทินสำเร็จหรือไม่ (ไม่ได้เก็บลงชีต)
return { ok:true, entry: clientEntry };
}
function deleteService_(id){
if(!id) return { ok:false, error:'ไม่พบรหัสรายการ' };
const sh = getServiceSheet_();
const list = sheetToObjects_(sh, SERVICE_HEADERS);
const found = list.find(x => x.id === id);
if(!found) return { ok:false, error:'ไม่พบรายการนี้ (อาจถูกลบไปแล้วจากเครื่องอื่น)' };
sh.deleteRow(found._row);
return { ok:true };
}
/* ══════════════════════════════════════════════ [เพิ่มใหม่] งานขอขนานไฟฟ้า (PowerRequest_data) ══════════════════════════════════════════════ */
function powerEntryForClient_(o){
const c = Object.assign({}, o);
delete c._row;
c.updatedAt = c.updatedAt instanceof Date ? c.updatedAt.toISOString() : String(c.updatedAt || '');
c.createdAt = c.createdAt instanceof Date ? c.createdAt.toISOString() : String(c.createdAt || '');
return c;
}
// หาแถวเดิมของลูกค้าคนนี้ในชีต — เรียงลำดับความสำคัญ: 1) รหัส id ตรงเป๊ะ (ตอนแก้ไขจากรายการ) 2) เลขที่ใบรับประกันตรงเป๊ะ
// (แม่นยำสุดถ้ามี) 3) เบอร์โทรตรงเป๊ะ (fallback ตอนไม่มีเลขที่ใบรับประกัน) — ไม่พบเลยถือว่าเป็นลูกค้าใหม่ จะสร้างแถวใหม่
function findExistingPowerRow_(list, id, warrantyNo, tel){
if(id){
const byId = list.find(x => x.id === id);
if(byId) return byId;
}
warrantyNo = String(warrantyNo || '').trim();
if(warrantyNo){
const byWarranty = list.find(x => String(x.warrantyNo || '').trim() === warrantyNo);
if(byWarranty) return byWarranty;
}
tel = String(tel || '').trim();
if(tel){
const byTel = list.find(x => String(x.tel || '').trim() === tel);
if(byTel) return byTel;
}
return null;
}
function savePowerRequest_(entry){
if(!entry) return { ok:false, error:'ไม่มีข้อมูลที่จะบันทึก' };
const sh = getPowerSheet_();
const list = sheetToObjects_(sh, POWER_HEADERS);
const customerName = String(entry.customerName || '').trim();
if(!customerName) return { ok:false, error:'กรุณากรอกชื่อลูกค้าก่อนบันทึก' };
const tel = String(entry.tel || '').trim();
const warrantyNo = String(entry.warrantyNo || '').trim();
const address = String(entry.address || '').trim();
const existing = findExistingPowerRow_(list, String(entry.id || '').trim(), warrantyNo, tel);
const id = existing ? existing.id : Utilities.getUuid();
const allowedAuthorities = ['MEA', 'PEA'];
const authority = allowedAuthorities.indexOf(entry.authority) !== -1 ? entry.authority : 'MEA';
const allowedStatuses = ['no_doc', 'missing_doc', 'pending_submit', 'submitted', 'success'];
const status = allowedStatuses.indexOf(entry.status) !== -1 ? entry.status : 'no_doc';
// เลขที่รับเรื่อง/หมายเหตุ เก็บตามที่ส่งมาเสมอ (ฝั่งหน้าเว็บซ่อน/แสดงช่องตามสถานะอยู่แล้ว ฝั่งนี้ไม่ต้องเคลียร์ทิ้งซ้ำ
// เผื่อผู้ใช้พิมพ์หมายเหตุไว้ก่อนเปลี่ยนสถานะเป็นสำเร็จ ข้อมูลเก่าจะได้ไม่หายไปเงียบ ๆ)
const caseNo = String(entry.caseNo || '').trim();
const note = String(entry.note || '').trim();
const recordedByName = String(entry.recordedByName || '').trim() || 'ไม่ระบุชื่อ';
const recordedByEmail = String(entry.recordedByEmail || '').trim();
const createdAt = existing ? existing.createdAt : new Date();
const now = new Date();
const row = [
id, warrantyNo, customerName, tel, address,
authority, status, caseNo, note,
recordedByName, recordedByEmail,
createdAt, now
];
if(existing){
sh.getRange(existing._row, 1, 1, POWER_HEADERS.length).setValues([row]);
} else {
sh.appendRow(row);
}
const saved = sheetToObjects_(sh, POWER_HEADERS).find(x => x.id === id);
return { ok:true, entry: powerEntryForClient_(saved) };
}
function deletePowerRequest_(id){
if(!id) return { ok:false, error:'ไม่พบรหัสรายการ' };
const sh = getPowerSheet_();
const list = sheetToObjects_(sh, POWER_HEADERS);
const found = list.find(x => x.id === id);
if(!found) return { ok:false, error:'ไม่พบรายการนี้ (อาจถูกลบไปแล้วจากเครื่องอื่น)' };
sh.deleteRow(found._row);
return { ok:true };
}
/* ══════════════════════════════════════════════ เลข RUN ใบรับประกัน (กันเลขซ้ำข้ามเครื่อง) ══════════════════════════════════════════════
เก็บไว้ในแท็บ WarrantySeq ของสเปรดชีตเดียวกัน แยกนับตามวันที่เริ่มรับประกัน (คอลัมน์ A = วันที่
YYYY-MM-DD, คอลัมน์ B = เลข RUN ล่าสุดที่ใช้ไปแล้วของวันนั้น) การ "reserve" (จองเลขจริง ตอนกดพิมพ์)
ต้องอยู่ภายใต้ withLock_ เสมอ กันสองเครื่องพิมพ์พร้อมกันแล้วได้เลขซ้ำกัน ส่วนการ "peek" (ดูตัวอย่าง
เฉย ๆ ตอนพิมพ์ยังไม่กด) อ่านได้อิสระไม่ต้องล็อก เพราะไม่ได้เพิ่มเลขจริง */
function findSeqRow_(sh, dateStr){
const lastRow = sh.getLastRow();
if(lastRow < 2) return null;
const values = sh.getRange(2, 1, lastRow - 1, 2).getValues();
for(let i = 0; i < values.length; i++){
if(String(values[i][0]).trim() === dateStr) return { row: i + 2, seq: Number(values[i][1]) || 0 };
}
return null;
}
function peekSeqForDate_(dateStr){
if(!dateStr) return 0;
const sh = getWarrantySeqSheet_();
const found = findSeqRow_(sh, dateStr);
return found ? found.seq + 1 : 1; // เลขถัดไปที่ "จะ" ได้ ถ้ากดพิมพ์ตอนนี้ (ยังไม่ได้จองจริง)
}
function reserveSeqForDate_(dateStr){
dateStr = String(dateStr || '').trim();
if(!dateStr) return { ok:false, error:'ไม่พบวันที่เริ่มรับประกัน' };
const sh = getWarrantySeqSheet_();
const found = findSeqRow_(sh, dateStr);
const nextSeq = found ? found.seq + 1 : 1;
if(found){
sh.getRange(found.row, 2).setValue(nextSeq);
} else {
sh.appendRow([dateStr, nextSeq]);
}
return { ok:true, seq: nextSeq };
}
ทำครั้งเดียว ครั้งถัดไปกดพิมพ์แล้วบันทึกอัตโนมัติเลย
function doGet(e) {
return ContentService
.createTextOutput('OK: warranty_card script is live. Use POST to upload files.')
.setMimeType(ContentService.MimeType.TEXT);
}
function getOrCreateFolder(parent, name) {
var it = parent.getFoldersByName(name);
if (it.hasNext()) return it.next();
return parent.createFolder(name);
}
function doPost(e) {
var debugLines = [];
function dbg(s) { debugLines.push(new Date().toISOString() + ' | ' + s); }
try {
dbg('doPost triggered');
var raw = (e.parameter && e.parameter.payload) || (e.postData && e.postData.contents) || '{}';
dbg('raw length: ' + raw.length + ' | postData exists: ' + !!e.postData + ' | parameter exists: ' + !!e.parameter);
var data = JSON.parse(raw);
dbg('parsed keys: ' + Object.keys(data).join(','));
var FOLDER_ID = '1FrCqK7GPrt4BgxyZ78qwSrx-M2RHV9_m';
var parentFolder = DriveApp.getFolderById(FOLDER_ID);
dbg('parent folder name: ' + parentFolder.getName());
var jobFolderName = (data.folder || 'unknown').toString();
var folder = getOrCreateFolder(parentFolder, jobFolderName);
dbg('job folder: ' + folder.getName() + ' url: ' + folder.getUrl());
if (data.pdf) {
var oldPdfs = folder.getFilesByType(MimeType.PDF);
while (oldPdfs.hasNext()) {
oldPdfs.next().setTrashed(true);
}
var bytes = Utilities.base64Decode(data.pdf);
dbg('decoded bytes: ' + bytes.length);
var blob = Utilities.newBlob(bytes, 'application/pdf', data.filename);
var file = folder.createFile(blob);
dbg('CREATED FILE: ' + file.getUrl());
return ContentService
.createTextOutput(JSON.stringify({success:true, url:file.getUrl(), name:data.filename}))
.setMimeType(ContentService.MimeType.JSON);
}
if (data.photo) {
var pbytes = Utilities.base64Decode(data.photo);
var mime = data.filename && data.filename.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg';
var pblob = Utilities.newBlob(pbytes, mime, data.filename);
var pfile = folder.createFile(pblob);
dbg('CREATED FILE: ' + pfile.getUrl());
return ContentService
.createTextOutput(JSON.stringify({success:true, url:pfile.getUrl(), name:data.filename}))
.setMimeType(ContentService.MimeType.JSON);
}
dbg('ERROR: No pdf or photo field in request body');
throw new Error('No pdf or photo field in request body');
} catch(err) {
dbg('ERROR: ' + err.toString());
return ContentService
.createTextOutput(JSON.stringify({success:false, error:err.toString()}))
.setMimeType(ContentService.MimeType.JSON);
}
}