// ✅ SaaS-КОМАНДА: ДОБАВЛЕНИЕ СТРОКИ НОВОГО СОТРУДНИКА В МАСТЕР-КОНТРОЛЬ
if (data.action === 'create_team_member') {
var mSheet = SpreadsheetApp.openById(MASTER_CONTROL_ID).getSheetByName('Пользователи');
var nextRow = mSheet.getLastRow() + 1;
// Пишем строго по колонкам Мастера: Логин | Пароль | ID таблицы | Компания | Роль | Родитель
mSheet.appendRow([
String(data.new_login).trim().toLowerCase(),
String(data.new_pass).trim(),
String(data.sheet_id).trim(),
String(data.comp_name || "Пользователь CRM").trim(),
String(data.new_role || "staff").trim().toLowerCase(),
String(data.parent_owner || "admin").trim().toLowerCase()
]);
SpreadsheetApp.flush();
return ContentService.createTextOutput(JSON.stringify({result:"success"})).setMimeType(ContentService.MimeType.JSON);
}
// 🗑️ SaaS-КОМАНДА: БЕЗОПАСНОЕ УДАЛЕНИЕ СОТРУДНИКА ИЗ МАСТЕР-КОНТРОЛЯ
if (data.action === 'delete_team_member') {
var mSheet = SpreadsheetApp.openById(MASTER_CONTROL_ID).getSheetByName('Пользователи');
var mData = mSheet.getDataRange().getValues();
var targetLog = String(data.target_login).trim().toLowerCase();
var parentSsId = String(data.sheet_id).trim();
// Бежим снизу вверх по Мастеру, ища совпадение логина и ID таблицы владельца
for (var i = mData.length - 1; i >= 1; i--) {
if (String(mData[i][0]).trim().toLowerCase() === targetLog && String(mData[i][2]).trim() === parentSsId) {
mSheet.deleteRow(i + 1); // Вычеркиваем строку сотрудника из базы Мастера
break;
}
}
SpreadsheetApp.flush();
return ContentService.createTextOutput(JSON.stringify({result:"success"})).setMimeType(ContentService.MimeType.JSON);
}
// =========================================================================
// 🚀 SaaS-ОБРАБОТЧИК АВТОМАТИЧЕСКОГО ПОДКЛЮЧЕНИЯ ДИСКА И КАЛЕНДАРЯ ИЗ CRM
// =========================================================================
else if (data.action === 'setup_google_resource') {
try {
var userSs = SpreadsheetApp.openById(data.sheet_id);
var settingsSheet = userSs.getSheetByName('Настройки') || userSs.getSheetByName('настройки');
if (!settingsSheet) {
return ContentService.createTextOutput("error: Лист Настройки не найден").setMimeType(ContentService.MimeType.TEXT);
}
var targetUserGmail = String(data.gmail || "").trim();
var responseText = "";
if (targetUserGmail !== "" && targetUserGmail.indexOf("@gmail.com") !== -1) {
settingsSheet.getRange('A2').setValue(targetUserGmail);
} else {
if (String(data.login).indexOf("@gmail.com") !== -1) {
settingsSheet.getRange('A2').setValue(data.login);
targetUserGmail = data.login;
}
}
} catch(eSetup) {
console.error("Ошибка в setup_google_resource: " + eSetup.toString());
}
SpreadsheetApp.flush();
}
// [НОВАЯ ЛОГИКА А]: СОХРАНЕНИЕ ID ПАПКИ КЛИЕНТА
if (data.type === 'drive') {
try {
var currentSheetId = data.sheet_id || data.sheetId || sheetId;
var targetWorkbook = SpreadsheetApp.openById(currentSheetId);
var settingsSheet = targetWorkbook.getSheetByName('Настройки') || targetWorkbook.getSheetByName('настройки');
if (settingsSheet) {
// Очищаем присланный клиентом ID папки от пробелов
var clientFolderId = String(data.folder_id || data.folderId || "").trim();
// Записываем ID папки клиента в ячейку F2 листа Настройки
settingsSheet.getRange("F2").setValue(clientFolderId);
responseText = "success_drive_linked:" + clientFolderId;
} else {
responseText = "error_settings_sheet_not_found";
}
} catch(eDrive) {
responseText = "error_linking_drive: " + eDrive.toString();
}
}
// [НОВАЯ ЛОГИКА Б]: СОХРАНЕНИЕ ID КАЛЕНДАРЯ КЛИЕНТА
else if (data.type === 'calendar') {
try {
var currentSheetId = data.sheet_id || data.sheetId || sheetId;
var targetWorkbook = SpreadsheetApp.openById(currentSheetId);
var settingsSheet = targetWorkbook.getSheetByName('Настройки') || targetWorkbook.getSheetByName('настройки');
if (settingsSheet) {
var clientCalendarId = String(data.calendar_id || data.calId || "").trim();
settingsSheet.getRange("E2").setValue(clientCalendarId);
responseText = "success_calendar_linked:" + clientCalendarId;
} else {
responseText = "error_settings_sheet_not_found";
}
} catch(eCal) {
responseText = "error_linking_calendar: " + eCal.toString();
}
}
// [ИСПРАВЛЕНО ДЛЯ ТИЛЬДЫ]: ПОЛУЧЕНИЕ СОХРАНЕННЫХ НАСТРОЕК (БЕЗ ОШИБОК CORS)
else if (data.type === 'get_settings') {
try {
var currentSheetId = data.sheet_id || data.sheetId || sheetId;
var targetWorkbook = SpreadsheetApp.openById(currentSheetId);
var settingsSheet = targetWorkbook.getSheetByName('Настройки') || targetWorkbook.getSheetByName('настройки');
var savedCalId = "";
var savedFolderId = "";
if (settingsSheet) {
savedCalId = String(settingsSheet.getRange("E2").getValue()).trim();
savedFolderId = String(settingsSheet.getRange("F2").getValue()).trim();
}
var result = {
status: "success",
calendar_id: (savedCalId === "undefined" || savedCalId === "null") ? "" : savedCalId,
folder_id: (savedFolderId === "undefined" || savedFolderId === "null") ? "" : savedFolderId
};
// ✅ Магия JSONP: если прилетел параметр callback, оборачиваем ответ в функцию
var callback = data.callback || "callback";
var jsonpResponse = callback + "(" + JSON.stringify(result) + ");";
return ContentService.createTextOutput(jsonpResponse)
.setMimeType(ContentService.MimeType.JAVASCRIPT);
} catch(eGet) {
var errCallback = data.callback || "callback";
return ContentService.createTextOutput(errCallback + "({status:'error'});")
.setMimeType(ContentService.MimeType.JAVASCRIPT);
}
}
// 📸 СБОРКА И ЭТАЛОННАЯ БЫСТРАЯ ЗАГРУЗКА ФОТО В ОБЛАКО ЯДРА (КАК БЫЛО В НАЧАЛЕ)//
var finalPhotoUrl = "";
var base64Data = String(data.file_base64 || "").trim();
if (!userSs && sheetId) {
userSs = SpreadsheetApp.openById(sheetId);
}
try {
var settingsSheet = userSs.getSheetByName('Настройки') || userSs.getSheetByName('настройки');
// Считываем ID персональной папки клиента из ячейки F2
var userFolderId = settingsSheet ? String(settingsSheet.getRange("F2").getValue()).trim() : "";
var folder = DriveApp.getRootFolder(); // По умолчанию берем корень
// Если у клиента указан корректный ID папки, пытаемся подключиться к ней
if (userFolderId !== "" && userFolderId !== "undefined" && userFolderId.length > 5) {
try {
folder = DriveApp.getFolderById(userFolderId);
} catch(eFindFolder) {
console.warn("Папка по ID не найдена, используем корневую: " + eFindFolder.toString());
}
}
// 2. Если данные картинки прилетели и папка готова — запускаем сохранение файла
if (base64Data !== "" && base64Data !== "undefined" && folder) {
var mimeType = "image/jpeg";
if (base64Data.indexOf(';base64,') !== -1) {
var base64Parts = base64Data.split(';base64,');
mimeType = base64Parts[0].replace("data:", "").trim();
base64Data = base64Parts[1];
} else {
base64Data = base64Data.replace(/^data:image\/(png|jpeg|jpg|webp);base64,/, "");
}
// Создаем один-единственный файл фотографии на вашем Диске Ядра Skitvl84
var decodedBlob = Utilities.newBlob(Utilities.base64Decode(base64Data), mimeType, data.file_name || "photo.jpg");
var uploadFile = folder.createFile(decodedBlob);
// Открываем публичный доступ по ссылке для отображения в интерфейсе CRM
try {
uploadFile.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
} catch(eSharing) {}
// Подключаем указанный email из А2 в редакторы этого файла (как было изначально)
try {
var clientTargetEmail = settingsSheet ? String(settingsSheet.getRange('A2').getValue()).trim() : "";
if (clientTargetEmail && clientTargetEmail.indexOf('@gmail.com') !== -1 && clientTargetEmail !== "sktvl84@gmail.com") {
uploadFile.addEditor(clientTargetEmail);
}
} catch(eAddEditor) {}
// Формируем чистую ссылку для записи в таблицу заказа
finalPhotoUrl = "https://drive.google.com/file/d/" + uploadFile.getId() + "/view";
}
} catch(eMainFile) {
Logger.log("Критический сбой загрузки файла: " + eMainFile.toString());
}
// =======================================================================
var orderId = "";
var client = String(data.name || data.client_name || '').trim();
var phone = String(data.phone || '').trim();
var status = String(data.status || 'Новое обращение').trim();
var eventN = String(data.event || data.event_name || '').trim();
var dateE = String(data.date_event || '');
var dateD = String(data.date_delivery || '');
var timeD = String(data.time_delivery || '').trim();
var products = String(data.products || '').trim();
var details = String(data.details || '').trim();
var address = String(data.address || '').trim();
var manager = String(data.login || 'admin').trim();
var discountPercent = Math.min(100, Math.max(0, parseInt(String(data.discount || "0").replace("%", ""), 10) || 0));
var deliveryNum = parseFloat(data.f_deliv_sum || data.delivery || data.f_deliv || 0) || 0;
var compositionSumNum = parseFloat(data.composition_sum) || 0;
var finalTotalCalculated = compositionSumNum - Math.round(compositionSumNum * (discountPercent / 100)) + deliveryNum;
var prepay = parseFloat(data.prepay) || 0;
var restDebt = finalTotalCalculated - prepay;
var rawChannel = data.channel || "";
if (rowNum > 0) {
orderId = dealsSheet.getRange(rowNum, 1).getValue().toString().trim();
var existingPhotos = "";
try {
if (rowNum > 0) {
existingPhotos = String(dealsSheet.getRange(rowNum, 20).getValue()).trim();
}
} catch(e){}
if (finalPhotoUrl) {
if (existingPhotos && existingPhotos !== "" && existingPhotos !== "undefined") {
finalPhotoUrl = existingPhotos + ", " + finalPhotoUrl;
}
} else {
finalPhotoUrl = (existingPhotos === "undefined") ? "" : existingPhotos;
}
dealsSheet.getRange(rowNum, 3).setValue(client); dealsSheet.getRange(rowNum, 4).setValue(phone);
dealsSheet.getRange(rowNum, 5).setValue(status); dealsSheet.getRange(rowNum, 6).setValue(eventN);
dealsSheet.getRange(rowNum, 7).setValue(dateE); dealsSheet.getRange(rowNum, 8).setValue(dateD);
dealsSheet.getRange(rowNum, 9).setValue(String(timeD).trim()).setNumberFormat('@'); dealsSheet.getRange(rowNum, 10).setValue(products);
dealsSheet.getRange(rowNum, 11).setValue(details); dealsSheet.getRange(rowNum, 12).setValue(address);
dealsSheet.getRange(rowNum, 13).setValue(compositionSumNum); dealsSheet.getRange(rowNum, 14).setValue(discountPercent).setNumberFormat('#');
dealsSheet.getRange(rowNum, 15).setValue(deliveryNum); dealsSheet.getRange(rowNum, 16).setValue(finalTotalCalculated);
dealsSheet.getRange(rowNum, 17).setValue(prepay); dealsSheet.getRange(rowNum, 18).setValue(restDebt);
dealsSheet.getRange(rowNum, 19).setValue(manager); dealsSheet.getRange(rowNum, 20).setValue(finalPhotoUrl);
dealsSheet.getRange(rowNum, 21).setValue(rawChannel); dealsSheet.getRange(rowNum, 22).setValue(data.badge || "");
if (data.calId) dealsSheet.getRange(rowNum, 23).setValue(data.calId);
} else {
orderId = "B" + Math.floor(100000 + Math.random() * 900000);
var nextRow = dealsSheet.getLastRow() + 1;
var rowValues = [orderId, new Date(), client, phone, status, eventN, dateE, dateD, String(timeD).trim(), products, details, address, compositionSumNum, discountPercent, deliveryNum, finalTotalCalculated, prepay, restDebt, manager, finalPhotoUrl, rawChannel, data.badge || "", data.calId || ""];
// Подстраховка: если ID календаря не прилетел, принудительно берем его из Настроек таблицы
if (!data.calId || data.calId === "" || data.calId === "undefined") {
try {
var settingsSheet = ss.getSheetByName('Настройки') || ss.getSheetByName('настройки');
data.calId = settingsSheet ? String(settingsSheet.getRange("E2").getValue()).trim() : "";
} catch(eCalGet) {}
}
// Пересобираем массив с уже обновленным ID календаря перед записью строки
rowValues = [orderId, new Date(), client, phone, status, eventN, dateE, dateD, String(timeD).trim(), products, details, address, compositionSumNum, discountPercent, deliveryNum, finalTotalCalculated, prepay, restDebt, manager, finalPhotoUrl, rawChannel, data.badge || "", data.calId || ""];
dealsSheet.appendRow(rowValues);
if (finalPhotoUrl && finalPhotoUrl !== "" && finalPhotoUrl !== "undefined") {
dealsSheet.getRange(nextRow, 20).setValue(finalPhotoUrl);
}
dealsSheet.getRange(nextRow, 9).setNumberFormat('@');
dealsSheet.getRange(nextRow, 14).setNumberFormat('#');
rowNum = nextRow;
/// ======= 📅 АВТОМАТИЧЕСКАЯ ОТПРАВКА ЗАКАЗА В КАЛЕНДАРЬ КЛИЕНТА =======
if (typeof dateD !== 'undefined' && dateD !== "" && dateD !== "undefined" && dateD !== null) {
try {
var settingsSheet = ss.getSheetByName('Настройки') || ss.getSheetByName('настройки');
var clientCalendarId = settingsSheet ? String(settingsSheet.getRange("E2").getValue()).trim() : "";
if (clientCalendarId !== "" && clientCalendarId !== "undefined" && clientCalendarId.length > 10) {
var targetCalendar = CalendarApp.getCalendarById(clientCalendarId);
if (targetCalendar) {
var eventTitle = "🎈 Заказ №" + orderId + " — " + client;
var eventDescription = "📞 Телефон: " + phone +
"\n⏰ Время доставки: " + String(timeD).trim() +
"\n📍 Адрес: " + address +
"\n📋 Состав: " + details +
"\n📊 Статус: " + status;
// 🔥 ПРАВИЛЬНЫЙ ПАРСИНГ ДАТЫ ФОРМАТА ДД.ММ.ГГГГ (Превращаем 14.06.2026 в понятный для JS объект)
var eventDateStart = new Date();
var dateParts = String(dateD).split('.');
if (dateParts.length === 3) {
// В JS месяцы идут от 0 до 11, поэтому вычитаем 1
eventDateStart = new Date(parseInt(dateParts[2], 10), parseInt(dateParts[1], 10) - 1, parseInt(dateParts[0], 10));
} else {
eventDateStart = new Date(dateD);
}
// 1. Корректное регулярное выражение для поиска времени ЧЧ:ММ
var timeMatch = String(timeD).match(/(\d{1,2}):(\d{2})/);
if (timeMatch) {
// 2. Достаем элементы массива совпадений по индексам [1] и [2] в 10-тичной системе
var hours = parseInt(timeMatch[1], 10);
var minutes = parseInt(timeMatch[2], 10);
eventDateStart.setHours(hours);
eventDateStart.setMinutes(minutes);
eventDateStart.setSeconds(0); // Очищаем секунды для красоты
// Создаем конечную точку события (+1 час по умолчанию)
var eventDateEnd = new Date(eventDateStart.getTime() + (1 * 60 * 60 * 1000));
targetCalendar.createEvent(eventTitle, eventDateStart, eventDateEnd, {description: eventDescription});
} else {
// Если точное время не найдено, создаем событие на весь день
targetCalendar.createAllDayEvent(eventTitle, eventDateStart, {description: eventDescription});
}
console.log("✅ Доставка заказа №" + orderId + " успешно добавлена.");
}
}
} catch(eCalSend) {
console.error("Ошибка календаря: " + eCalSend.toString());
}
}
// Проверка увеличения предоплаты для фиксации в кассу
var oldPrepay = 0; try { if (rowNum > 0) oldPrepay = Number(dealsSheet.getRange(rowNum, 17).getValue()) || 0; } catch(e){}
if (prepay > oldPrepay) {
var amountToRegister = prepay - oldPrepay;
var paySheet = ss.getSheetByName('реестр_платежей') || ss.insertSheet('реестр_платежей');
if (paySheet.getLastRow() === 0) paySheet.appendRow(['Дата внесения', 'ID Заказа', 'Клиент', 'Сумма платежа']);
paySheet.appendRow([new Date(), orderId, client, amountToRegister]);
}
// =========================================================================
// 🔥 ПРЯМОЙ ИЗОЛИРОВАННЫЙ ПРОБРОС НА ЛИСТ "БАЗА_КЛИЕНТОВ" (Строка 1089)
// =========================================================================
try {
var targetUserSheetId = data.sheet_id || data.sheetId || (typeof sheetId !== 'undefined' ? sheetId : "");
if (targetUserSheetId) {
var activeUserSpreadsheet = SpreadsheetApp.openById(targetUserSheetId);
// СТРОГО ВАШ РЕАЛЬНЫЙ ЛИСТ ИЗ CRM v50.0
var interfaceClientsSheet = activeUserSpreadsheet.getSheetByName('база_клиентов');
if (interfaceClientsSheet) {
var orderClientName = String(data.name || data.client_name || (typeof client !== 'undefined' ? client : "Без имени")).trim();
var orderClientPhone = String(data.phone || data.client_phone || (typeof phone !== 'undefined' ? phone : "")).trim();
if (orderClientPhone && orderClientPhone !== "") {
// Последний параметр true означает, что запись идет из заказа
smartCustomerUpsert(interfaceClientsSheet, orderClientName, orderClientPhone, true);
}
}
}
} catch (errSync) {
Logger.log("Заминка изолированного автопроброса клиента в doPost: " + errSync.toString());
}
// =========================================================================
} // ◄ ПОСТАВЬТЕ ЭТУ ФИГУРНУЮ СКОБКУ СЮДА!
return ContentService.createTextOutput(JSON.stringify({result:"success", row: rowNum, photo: finalPhotoUrl})).setMimeType(ContentService.MimeType.JSON);
} catch(f) { return ContentService.createTextOutput(JSON.stringify({result:"error", error: f.toString()})).setMimeType(ContentService.MimeType.JSON); }
}
// ✅ МЕТОД ЯДРА: СОЗДАНИЕ ПАПКИ С ЖЕСТКОЙ ВЕРИФИКАЦИЕЙ ПРАВ ВЛАДЕЛЬЦА (СЦЕНАРИЙ 2)
function coreSetupUserGoogleDrive(activeSsId, currentUserEmail) {
if (!activeSsId) return "error: no sheet id";
try {
var ss = SpreadsheetApp.openById(activeSsId);
var settingsSheet = ss.getSheetByName('Настройки') || ss.getSheetByName('настройки');
if (!settingsSheet) return "error: Лист 'Настройки' не найден в таблице!";
var ownerEmail = ss.getOwner() ? ss.getOwner().getEmail() : "";
var currentEmail = String(currentUserEmail || "").toLowerCase().trim();
var cleanOwnerEmail = String(ownerEmail).toLowerCase().trim();
if (currentEmail !== cleanOwnerEmail && cleanOwnerEmail !== "") { return "access_denied:" + ownerEmail; }
var currentFolderId = String(settingsSheet.getRange("F2").getValue()).trim();
if (currentFolderId && currentFolderId !== "" && currentFolderId !== "undefined" && currentFolderId.length > 5) { return "already_configured"; }
var folderName = "📸 Фото заказов — " + ss.getName().replace("Шаблон ", "");
var newFolder = DriveApp.createFolder(folderName);
newFolder.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
var folderId = newFolder.getId();
settingsSheet.getRange("F2").setValue(folderId);
SpreadsheetApp.flush();
return "success:" + folderName + "|" + newFolder.getUrl();
} catch(e) { return "error: " + e.toString(); }
}
// =========================================================================
// 🚀 ЕДИНЫЙ ЦЕНТРАЛЬНЫЙ SaaS-ДВИЖОК ТАБЛИЧНОГО КАЛЬКУЛЯТОРА v69.0
// =========================================================================
// 1. Централизованная выгрузка прайс-листа товаров для сайдбара
// ✅ ЭТАЛОННАЯ SaaS-ВЫГРУЗКА ПРАЙС-ЛИСТА С АВТО-ДЕТЕКЦИЕЙ ГРУПП v72.0
function coreSavePrices(sheetId, clientCatalog) {
if (!sheetId || !clientCatalog) return "error_bad_data";
try {
var catalogArr = clientCatalog;
if (typeof clientCatalog === "string") {
var decodedString = clientCatalog;
try { while (decodedString.indexOf('%') !== -1) { decodedString = decodeURIComponent(decodedString); } } catch(e) {}
catalogArr = JSON.parse(decodedString);
}
if (!Array.isArray(catalogArr)) return "error_not_array";
var ss = SpreadsheetApp.getActiveSpreadsheet();
var goodsSheet = ss.getSheetByName('Товары') || ss.getSheetByName('товары');
if (!goodsSheet) return "error_no_sheet";
var lastRow = goodsSheet.getLastRow();
if (lastRow > 0) { goodsSheet.getRange(1, 1, lastRow, 2).clearContent(); }
var rowsToPush = [["", ""]]; var yellowRows = [];
catalogArr.forEach(function(item) {
if (!item || !item.name) return;
var colA = String(item.name).trim();
if (item.isGroup === true || item.isGroup === "true") {
rowsToPush.push([colA, ""]); yellowRows.push(rowsToPush.length);
} else {
// Парсим цену как дробное число (поддержка центов меньше 1)
var priceNum = parseFloat(String(item.price).replace(/[^0-9.,]/g, '').replace(',', '.')) || 0;
rowsToPush.push([colA, priceNum]);
}
});
goodsSheet.getRange(1, 1, rowsToPush.length, 2).setValues(rowsToPush);
if (lastRow > 0) { goodsSheet.getRange(1, 1, lastRow, 2).setBackground("#ffffff").setFontWeight("normal"); }
yellowRows.forEach(function(rowIndex) { goodsSheet.getRange(rowIndex, 1, 1, 2).setBackground("#ffff00").setFontWeight("bold"); });
SpreadsheetApp.flush();
return "success";
} catch(e) { return "error: " + e.toString(); }
}
// 2. Универсальный сборщик SaaS-пакета инициализации калькулятора
function getSaaSAppInitPack() {
var output = { sheetId: "", catalog: [] };
try {
var activeSs = SpreadsheetApp.getActiveSpreadsheet();
output.sheetId = activeSs.getId();
output.catalog = coreGetPrices(output.sheetId);
} catch(e) {}
return output;
}
// 🎯 ОЧИЩЕНО: Физическая запись перенесена в локальные файлы таблиц клиентов для обхода изоляции Google
function coreWriteComposition(sheetId, selectedItemsJson, totalSumStatic) {
return "success";
}