/** * Khóa toàn bộ sheet KHO: không cho sửa tay, chỉ script ghi dữ liệu. * Lưu ý: bạn (tài khoản đang chạy script) vẫn là editor được phép sửa. */ function khoaSheetKHO() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheetKho = ss.getSheetByName("KHO"); if (!sheetKho) return; const protections = sheetKho.getProtections(SpreadsheetApp.ProtectionType.SHEET); protections.forEach(p => p.remove()); const protection = sheetKho.protect().setDescription("Khóa sheet KHO"); protection.setWarningOnly(false); const me = Session.getEffectiveUser(); protection.removeEditors(protection.getEditors()); protection.addEditor(me); // Không chừa ô nào để sửa tay (chỉ bạn – editor – có thể chỉnh) protection.setUnprotectedRanges([]); } /** * Gắn trigger On form submit cho sheet Form_KHO. */ function ganTriggerFormKHO() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const triggers = ScriptApp.getProjectTriggers(); triggers.forEach(t => { if (t.getHandlerFunction() === "xuLyFormKho") ScriptApp.deleteTrigger(t); }); ScriptApp.newTrigger("xuLyFormKho") .forSpreadsheet(ss) .onFormSubmit() .create(); } /** * Gắn trigger time-based để auto quét sinh mã định kỳ. * Ví dụ: mỗi 15 phút. */ function ganTriggerAutoGenerate() { const triggers = ScriptApp.getProjectTriggers(); triggers.forEach(t => { if (t.getHandlerFunction() === "autoGenerateMaSP") ScriptApp.deleteTrigger(t); }); ScriptApp.newTrigger("autoGenerateMaSP") .timeBased() .everyMinutes(15) .create(); } /** * Menu tiện lợi. */ function onOpen() { const ui = SpreadsheetApp.getUi(); ui.createMenu("KHO - Quản trị") .addItem("Khóa toàn bộ sheet KHO", "khoaSheetKHO") .addItem("Gắn trigger Form_KHO", "ganTriggerFormKHO") .addItem("Gắn trigger AutoGenerate", "ganTriggerAutoGenerate") .addToUi(); } /** * Xử lý response từ Form_KHO (On form submit). * - NHẬP MỚI: thêm dòng, sinh mã duy nhất. * - CHỈNH SỬA: cập nhật; nếu đổi PHÂN LOẠI/SIZE/KHO thì sinh lại mã duy nhất. * - Ghi trạng thái vào cột Y (25) đúng hàng vừa submit. */ function xuLyFormKho(e) { try { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheetKho = ss.getSheetByName("KHO"); const sheetForm = e.source.getSheetByName("Form_KHO"); if (!sheetKho || !sheetForm) return; // Hàng vừa submit trong Form_KHO const formRow = e.range ? e.range.getRow() : sheetForm.getLastRow(); // Bảo đảm có cột Y (25) để ghi trạng thái const maxCols = sheetForm.getMaxColumns(); if (maxCols < 25) { sheetForm.insertColumnsAfter(maxCols, 25 - maxCols); } // Khóa KHO trước khi ghi (bạn vẫn là editor bảo vệ) khoaSheetKHO(); const data = e.values; // mảng giá trị theo thứ tự cột của Form_KHO const headerForm = sheetForm.getRange(1, 1, 1, Math.max(data.length, sheetForm.getLastColumn())).getValues()[0]; const mapForm = {}; headerForm.forEach((h, i) => mapForm[String(h).trim().toUpperCase()] = i); const chucNang = String(data[mapForm["CHỨC NĂNG"]] || "").trim().toUpperCase(); let status = ""; // ===== NHẬP MỚI ===== if (chucNang === "NHẬP MỚI") { const tenMau = (data[mapForm["TÊN MẪU"]] || "").trim(); const mauSac = (data[mapForm["MÀU SẮC"]] || "").trim(); const tenMauFull = tenMau + (mauSac ? " " + mauSac : ""); const lastRow = sheetKho.getLastRow(); const tenMauColValues = lastRow > 1 ? sheetKho.getRange(2, 1, lastRow - 1).getValues().map(x => String(x[0] || "").trim().toUpperCase()) : []; if (tenMauFull && tenMauColValues.includes(tenMauFull.toUpperCase())) { status = "⚠️ Trùng sản phẩm"; } else { const phanLoai = String(data[mapForm["PHÂN LOẠI"]] || "").trim(); const size = String(data[mapForm["SIZE"]] || "").trim(); const kho = String(data[mapForm["KHO"]] || "").trim(); const baseCode = normalizePhanLoai(phanLoai) + "-" + normalizeSize(size) + "-" + normalizeKho(kho); const allCodes = lastRow > 1 ? sheetKho.getRange(2, 2, lastRow - 1).getValues().map(x => String(x[0] || "").trim()) : []; const uniqueCode = makeUniqueCode(baseCode, new Set(allCodes)); sheetKho.appendRow([ tenMauFull, // A: TÊN MẪU uniqueCode, // B: MÃ SẢN PHẨM phanLoai, // C: PHÂN LOẠI size, // D: SIZE data[mapForm["SỐ LƯỢNG"]] || "", // E: SỐ LƯỢNG kho, // F: KHO data[mapForm["KỆ"]] || "", // G: KỆ data[mapForm["TẦNG"]] || "", // H: TẦNG data[mapForm["NAM/ NỮ"]] || "", // I: NAM/ NỮ data[mapForm["GIÁ THUÊ"]] || "", // J: GIÁ THUÊ data[mapForm["HÌNH ẢNH"]] || "" // K: HÌNH ẢNH ]); status = "✅ ĐÃ THÊM MỚI"; } } // ===== CHỈNH SỬA ===== if (chucNang === "CHỈNH SỬA") { const maSPForm = String(data[mapForm["MÃ SẢN PHẨM"]] || "").trim(); if (!maSPForm) { status = "❌ Không tìm thấy mã sản phẩm"; } else { const dataKho = sheetKho.getDataRange().getValues(); if (!dataKho || dataKho.length < 2) { status = "❌ KHO trống"; } else { const headerKho = dataKho[0].map(h => String(h).trim().toUpperCase()); const mapKho = {}; headerKho.forEach((h, i) => mapKho[h] = i); let foundRow = -1; for (let i = 1; i < dataKho.length; i++) { if (String(dataKho[i][mapKho["MÃ SẢN PHẨM"]]).trim() === maSPForm) { foundRow = i + 1; // vị trí trên sheet (1-based) break; } } if (foundRow === -1) { status = "❌ Không tìm thấy mã sản phẩm"; } else { // Giá trị hiện có const currentPhanLoai = String(sheetKho.getRange(foundRow, mapKho["PHÂN LOẠI"] + 1).getValue() || "").trim(); const currentSize = String(sheetKho.getRange(foundRow, mapKho["SIZE"] + 1).getValue() || "").trim(); const currentKho = String(sheetKho.getRange(foundRow, mapKho["KHO"] + 1).getValue() || "").trim(); // Giá trị mới từ form (nếu có) const newPhanLoai = mapForm["PHÂN LOẠI (CS)"] != null ? String(data[mapForm["PHÂN LOẠI (CS)"]] || "").trim() : ""; const newSize = mapForm["SIZE (CS)"] != null ? String(data[mapForm["SIZE (CS)"]] || "").trim() : ""; const newKho = mapForm["KHO (CS)"] != null ? String(data[mapForm["KHO (CS)"]] || "").trim() : ""; // Cập nhật các trường khác function update(colForm, colKho) { if (mapForm[colForm] != null && mapKho[colKho] != null) { const val = data[mapForm[colForm]]; if (val !== "" && val != null) { sheetKho.getRange(foundRow, mapKho[colKho] + 1).setValue(val); } } } update("PHÂN LOẠI (CS)", "PHÂN LOẠI"); update("SIZE (CS)", "SIZE"); update("SỐ LƯỢNG (CS)", "SỐ LƯỢNG"); update("KHO (CS)", "KHO"); update("KỆ (CS)", "KỆ"); update("TẦNG (CS)", "TẦNG"); update("NAM/ NỮ (CS)", "NAM/ NỮ"); update("GIÁ THUÊ (CS)", "GIÁ THUÊ"); update("HÌNH ẢNH (CS)", "HÌNH ẢNH"); // Nếu thay đổi PHÂN LOẠI/SIZE/KHO ⇒ sinh lại mã duy nhất const finalPhanLoai = newPhanLoai || currentPhanLoai; const finalSize = newSize || currentSize; const finalKho = newKho || currentKho; const changedIdentity = (newPhanLoai && newPhanLoai !== currentPhanLoai) || (newSize && newSize !== currentSize) || (newKho && newKho !== currentKho); if (changedIdentity) { const baseCode = normalizePhanLoai(finalPhanLoai) + "-" + normalizeSize(finalSize) + "-" + normalizeKho(finalKho); // Tập mã hiện có (ngoại trừ chính dòng đang chỉnh) const lastRow = sheetKho.getLastRow(); const allCodes = lastRow > 1 ? sheetKho.getRange(2, 2, lastRow - 1).getValues().map(x => String(x[0] || "").trim()) : []; const codeSet = new Set(allCodes.filter(code => code !== maSPForm)); const newUniqueCode = makeUniqueCode(baseCode, codeSet); sheetKho.getRange(foundRow, mapKho["MÃ SẢN PHẨM"] + 1).setValue(newUniqueCode); status = "✏️ ĐÃ CHỈNH SỬA (ĐỔI MÃ)"; } else { status = "✏️ ĐÃ CHỈNH SỬA"; } } } } } // ===== Ghi trạng thái vào cột Y (25) đúng hàng vừa submit ===== if (status) { sheetForm.getRange(formRow, 25).setValue(status); } } catch (err) { Logger.log("xuLyFormKho error: " + err); } } /** * Trigger khi chỉnh sửa trực tiếp trên tab KHO: * Nếu MÃ SẢN PHẨM trống và đủ PHÂN LOẠI, SIZE, KHO thì tự sinh. */ function onEdit(e) { const sheet = e.range.getSheet(); if (sheet.getName() !== "KHO") return; const row = e.range.getRow(); if (row < 2) return; const header = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0] .map(h => String(h).trim().toUpperCase()); const map = {}; header.forEach((h,i)=>map[h]=i); const colMaSP = map["MÃ SẢN PHẨM"]; const colPL = map["PHÂN LOẠI"]; const colSize = map["SIZE"]; const colKho = map["KHO"]; // Nếu thiếu cột cần thiết thì thoát if ([colMaSP, colPL, colSize, colKho].some(v => v == null || v < 0)) return; const maSP = String(sheet.getRange(row, colMaSP+1).getValue()||"").trim(); if (!maSP) { const phanLoai = String(sheet.getRange(row, colPL+1).getValue()||"").trim(); const size = String(sheet.getRange(row, colSize+1).getValue()||"").trim(); const kho = String(sheet.getRange(row, colKho+1).getValue()||"").trim(); if (phanLoai && size && kho) { const allCodes = sheet.getRange(2, colMaSP+1, Math.max(0, sheet.getLastRow()-1)) .getValues().map(x=>String(x[0]||"").trim()).filter(x=>x); const codeSet = new Set(allCodes); const baseCode = normalizePhanLoai(phanLoai) + "-" + normalizeSize(size) + "-" + normalizeKho(kho); const newCode = makeUniqueCode(baseCode, codeSet); sheet.getRange(row, colMaSP+1).setValue(newCode); } } } /** * Quét toàn bộ sheet KHO định kỳ để sinh mã cho dòng chưa có. */ function autoGenerateMaSP() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheetKho = ss.getSheetByName("KHO"); if (!sheetKho) return; const data = sheetKho.getDataRange().getValues(); if (data.length < 2) return; const header = data[0].map(h => String(h).trim().toUpperCase()); const map = {}; header.forEach((h,i)=>map[h]=i); const colMaSP = map["MÃ SẢN PHẨM"]; const colPL = map["PHÂN LOẠI"]; const colSize = map["SIZE"]; const colKho = map["KHO"]; if ([colMaSP, colPL, colSize, colKho].some(v => v == null || v < 0)) return; // Tập mã hiện có const codeSet = new Set(); for (let r=1;r