GAS 14
// ============================================================================
// TRIPOD DATA - OPTIMIZED V14 (STABLE)
// ============================================================================
function doGet(e) { return handleRequest(e); }
function doPost(e) { return handleRequest(e); }
function handleRequest(e) {
// LockService giúp ngăn chặn 2 người cùng lưu 1 lúc gây lỗi dữ liệu
var lock = LockService.getScriptLock();
if (lock.tryLock(10000)) { // Đợi tối đa 10s
try {
var params = e.parameter;
var action = params.action;
var ss = SpreadsheetApp.getActiveSpreadsheet();
var appPrefix = params.appPrefix || "Target";
// --- XỬ LÝ ĐĂNG NHẬP / ĐĂNG KÝ ---
if (action === 'login') return responseJSON(loginUser(ss, params.username, params.password, appPrefix, params.headerNames));
if (action === 'register') return responseJSON(registerUser(ss, params.username, params.password, appPrefix, params.headerNames));
var sheetName = params.targetSheetName;
if (!sheetName) throw new Error("Missing Sheet Name");
// --- TẢI DỮ LIỆU (READ) ---
if (action === 'loadData') {
var sheet = ss.getSheetByName(sheetName);
if (!sheet) return responseJSON([]);
var data = sheet.getDataRange().getValues();
if (data.length > 1) data.shift(); else data = []; // Bỏ dòng Header
return responseJSON(data);
}
// --- CÁC HÀM GHI (WRITE) ---
var sheet = getOrCreateSheet(ss, sheetName, params.headerNames);
// 1. INSERT (THÊM MỚI)
if (action === 'insert') {
var rowData = parseGenericRowData(params);
var inputId = rowData[0].toString();
// Nếu ID rỗng hoặc là ID tạm (TEMP_), tạo ID thật (UUID)
var realId = (!inputId || inputId.startsWith("'TEMP_") || inputId === "'")
? "'" + Utilities.getUuid()
: inputId;
rowData[0] = realId; // Gán ID thật vào dữ liệu
sheet.appendRow(rowData);
// Trả về ID thật để Web cập nhật lại
return responseJSON({ result: 'success', id: realId });
}
// 2. UPDATE (CẬP NHẬT)
else if (action === 'update') {
var id = params.id;
if (!id) throw new Error("Missing ID for update");
// Tìm dòng cần sửa (Tối ưu: Chỉ tìm trong cột A)
var rowIndex = findRowIndexByIdOptimized(sheet, id);
if (rowIndex == -1) throw new Error("ID not found on Server: " + id);
var rowData = parseGenericRowData(params);
rowData[0] = forceText(id); // Giữ nguyên ID cũ
// Ghi đè dữ liệu mới vào dòng đó
sheet.getRange(rowIndex, 1, 1, rowData.length).setValues([rowData]);
return responseJSON({ result: 'success' });
}
// 3. DELETE (XÓA)
else if (action === 'delete') {
var id = params.id;
if (!id) throw new Error("Missing ID for delete");
var rowIndex = findRowIndexByIdOptimized(sheet, id);
if (rowIndex == -1) {
// Nếu không tìm thấy, báo success giả để web không báo lỗi (vì đằng nào cũng muốn xóa)
return responseJSON({ result: 'success', message: 'ID not found but ignored' });
}
sheet.deleteRow(rowIndex);
return responseJSON({ result: 'success' });
}
} catch (e) {
return responseJSON({ result: 'error', message: e.toString() });
} finally {
lock.releaseLock();
}
} else {
return responseJSON({ result: 'error', message: 'Server is busy, try again.' });
}
}
// === CÁC HÀM HỖ TRỢ ĐÃ TỐI ƯU ===
// Hàm tìm dòng nhanh: Chỉ đọc cột A thay vì đọc cả bảng
function findRowIndexByIdOptimized(sheet, id) {
var cleanId = cleanText(id);
// Lấy dữ liệu cột A (Cột ID)
var lastRow = sheet.getLastRow();
if (lastRow < 2) return -1;
var idColumn = sheet.getRange(1, 1, lastRow, 1).getValues(); // Mảng 2 chiều [[id1], [id2]...]
// Duyệt ngược từ dưới lên (thường dữ liệu mới ở dưới, tìm sẽ nhanh hơn)
for (var i = idColumn.length - 1; i >= 1; i--) {
if (cleanText(idColumn[i][0]) == cleanId) {
return i + 1; // Vì index mảng bắt đầu từ 0, dòng sheet bắt đầu từ 1
}
}
return -1;
}
function cleanText(val) {
if (val == null) return "";
var str = val.toString();
// Xóa dấu nháy đơn ở đầu nếu có (để so sánh chính xác)
return str.startsWith("'") ? str.substring(1) : str;
}
function forceText(val) {
// Thêm dấu nháy đơn để ép Google Sheet hiểu là Text (tránh lỗi định dạng ngày tháng/số)
return "'" + cleanText(val);
}
function parseGenericRowData(params) {
if (!params.fieldNames) return [];
var fields = params.fieldNames.split(',');
return fields.map(function(f) {
// ID (cột đầu tiên) xử lý riêng ở hàm main, ở đây cứ lấy value
var val = params[f];
return (val === undefined || val === null) ? "" : forceText(val);
});
}
function responseJSON(obj) {
return ContentService.createTextOutput(JSON.stringify(obj)).setMimeType(ContentService.MimeType.JSON);
}
function getOrCreateSheet(ss, name, headers) {
var sheet = ss.getSheetByName(name);
if (!sheet) {
sheet = ss.insertSheet(name);
if (headers) {
var h = headers.split(',');
sheet.appendRow(h);
sheet.getRange(1,1,1,h.length).setFontWeight("bold").setBackground("#f3f3f3");
sheet.setFrozenRows(1);
}
}
return sheet;
}
// Giữ nguyên logic User
function loginUser(ss, u, p, prefix, headers) {
var userSheet = getOrCreateSheet(ss, "Users", "Username,Password,CreatedDate");
var data = userSheet.getDataRange().getValues();
var cU = cleanText(u), cP = cleanText(p);
for(var i=1; i