/// =========================================================================
// ⚙ SaaS-ОБРАБОТЧИК РЕГИСТРАЦИИ (ВСТРАИВАЕТСЯ В КОНЕЦ ФАЙЛА BAZA.GS) v50.0
// =========================================================================
function handleSaaSRegistration(data) {
var MASTER_CONTROL_ID = '15lc3jj5D4eadrrx0amG-rOE0yCPo2D1P51Hu-T-aV0c'; // ID Мастер-Таблицы
var TEMPLATE_SHEET_ID = '1kDri-evB3XCaRitYY73WwoRa0CaaXshb3URUyEaKOrU'; // ID вашей идеальной таблицы-донора
var email = String(data.email || '').trim().toLowerCase();
var password = String(data.password || '').trim();
var compName = String(data.compName || 'Студия аэродизайна').trim();
if (!email || !password) {
return ContentService.createTextOutput(JSON.stringify({ success: false, error: "Заполните Email и Пароль!" })).setMimeType(ContentService.MimeType.JSON);
}
var controlSheet = SpreadsheetApp.openById(MASTER_CONTROL_ID).getSheetByName('Пользователи');
var usersData = controlSheet.getDataRange().getValues();
// 1. Проверка на дубликаты
for (var i = 1; i < usersData.length; i++) {
if (String(usersData[i][0]).toLowerCase().trim() === email) {
return ContentService.createTextOutput(JSON.stringify({ success: false, error: "Этот Email уже зарегистрирован в CRM!" })).setMimeType(ContentService.MimeType.JSON);
}
}
try {
// 2. Клонируем ваш автономный шаблон
var templateFile = DriveApp.getFileById(TEMPLATE_SHEET_ID);
// 📂 АВТОМАТИЧЕСКАЯ ОРГАНИЗАЦИЯ: Подключаем вашу целевую папку Диска
var targetFolder = DriveApp.getFolderById('1ascecLnBm0h6lQKLW6bq0OfPffxYtRDZ');
// Создаем копию шаблона-донора сразу внутри этой конкретной папки
var newFile = templateFile.makeCopy("Balloon CRM База — " + email, targetFolder);
var newSheetId = newFile.getId();
// ✅ НАДЕЖНЫЙ SaaS-ШЕРИНГ ДЛЯ DRIVE API v3 БЕЗ СИСТЕМНЫХ УВЕДОМЛЕНИЙ GOOGLE
// 1. БЕЗОПАСНЫЙ ШЕРИНГ ТАБЛИЦЫ КЛИЕНТУ
var fileId = newFile.getId();
try {
// Делаем тихий шеринг через Drive API
Drive.Permissions.create({
'role': 'editor',
'type': 'user',
'value': email,
'emailAddress': email
}, fileId, {
'sendNotificationEmail': false,
'supportsAllDrives': true
});
} catch(eShare) {
Logger.log("Ошибка Drive API: " + eShare.toString());
// Если служба упала — открываем тихий доступ по ссылке редактора (БЕЗ СЕРЫХ ПИСЕМ GOOGLE!)
try {
newFile.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.EDIT);
} catch(eFallback) {}
}
// 2. РАСЧЕТ 7 ДНЕЙ БЕСПЛАТНОГО ТРИАЛА
var dateRegistered = new Date();
var dateExpired = new Date();
dateExpired.setDate(dateRegistered.getDate() + 7);
var formattedExpDate = Utilities.formatDate(dateExpired, Session.getScriptTimeZone(), "dd.MM.yyyy");
// 3. ЗАПИСЬ НОВОГО ПОЛЬЗОВАТЕЛЯ В МАСТЕР-ТАБЛИЦУ УПРАВЛЕНИЯ
controlSheet.appendRow([
email,
password,
fileId,
compName,
"owner",
"SaaS_System",
formattedExpDate,
"trial",
"PENDING" // ◄ Колонка I (индекс 8): Сигнал для триггера, что письмо нужно отправить!
]);
// 🔔 МГНОВЕННОЕ УВЕДОМЛЕНИЕ ДЛЯ ВЛАДЕЛЬЦА CRM v50.0
try {
var adminEmail = "vladballoon@mail.ru"; // ◄ ВПИСАЛ ВАШУ РАБОЧУЮ ПОЧТУ!
var adminSubject = "🚀 Новая регистрация в Balloon CRM: " + compName;
var adminBody = "🎉 Ура! На платформе зарегистрировался новый пользователь.\n\n" +
"👤 Email/Логин: " + email + "\n" +
"🎈 Студия: " + compName + "\n" +
"📊 Ссылка на созданную базу: https://docs.google.com/spreadsheets/d/" + fileId + "\n\n" +
"Проверьте Мастер-Таблицу, строка успешно создана со статусом PENDING.";
GmailApp.sendEmail(adminEmail, adminSubject, adminBody);
} catch(eAdmin) {
Logger.log("Ошибка отправки уведомления админу: " + eAdmin.toString());
}
return ContentService.createTextOutput(JSON.stringify({ success: true, sheetId: fileId, compName: compName, role: "owner" })).setMimeType(ContentService.MimeType.JSON);
} catch(errCopy) {
return ContentService.createTextOutput(JSON.stringify({ success: false, error: "Ошибка генерации базы: " + errCopy.toString() })).setMimeType(ContentService.MimeType.JSON);
}
}
function crmBackgroundMailSender() {
var MASTER_CONTROL_ID = '15lc3jj5D4eadrrx0amG-rOE0yCPo2D1P51Hu-T-aV0c';
var controlSheet = SpreadsheetApp.openById(MASTER_CONTROL_ID).getSheetByName('Пользователи');
// ======= 📨 АДМИНИСТРАТИВНЫЙ ШЛЮЗ МАССОВОЙ РАССЫЛКИ (СТОЛБЕЦ L) =======
try {
if (typeof controlSheet !== 'undefined' && controlSheet) {
var lastMasterRow = controlSheet.getLastRow();
if (lastMasterRow >= 2) {
// Запрашиваем диапазон до 12 столбца (L) включительно
var masterRange = controlSheet.getRange(2, 1, lastMasterRow - 1, 12);
var masterValues = masterRange.getValues();
for (var m = 0; m < masterValues.length; m++) {
var masterRowNum = m + 2;
// Предполагаем, что Email пользователя лежит в столбце B (индекс 1)
// Если почта в другом столбце — измените индекс 1 ниже на нужный
var userEmail = String(masterValues[m][0]).trim().toLowerCase();
// Столбец L — это 12-й столбец (индекс в массиве 11)
var mailCommand = String(masterValues[m][11]).trim();
if (mailCommand === "SEND_NEWS" && userEmail !== "") {
var emailSubject = "📢 Ваша подписка скоро закончится";
var mailSentSuccessfully = false;
try {
GmailApp.sendEmail(userEmail, emailSubject, "Включите отображение HTML-писем.", {
htmlBody: getFormattedAdminNewsHtml(),
name: "Balloon CRM SaaS Platform"
});
mailSentSuccessfully = true;
} catch(eMailObj) {
console.error("Ошибка отправки рассылки на почту " + userEmail + ": " + eMailObj.toString());
}
// После отправки затираем команду прямо в столбце L (12), записывая дату
if (mailSentSuccessfully) {
controlSheet.getRange(masterRowNum, 12).setValue("Отправлено: " + Utilities.formatDate(new Date(), "GMT+3", "dd.MM.yyyy HH:mm"));
} else {
controlSheet.getRange(masterRowNum, 12).setValue("Ошибка отправки");
}
}
}
}
}
} catch(eGlobalMailGate) {
console.error("Критическая ошибка шлюза рассылки в столбце L: " + eGlobalMailGate.toString());
}
// ===============================================================================
var data = controlSheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
var row = data[i];
// Если в колонке I (индекс 8) стоит статус PENDING — отправляем письмо!
if (row && row.length >= 9 && String(row[8]).trim() === "PENDING") {
var email = row[0];
var fileId = row[2];
var password = row[1];
var compName = row[3];
var formattedExpDate = row[6];
try {
var tableUrl = "https://docs.google.com/spreadsheets/d/" + fileId + "/edit";
var emailBody =
'<div style="font-family:Arial,sans-serif;max-width:600px;color:#333;line-height:1.5;">' +
'<h2 style="color:#1a73e8;">Добро пожаловать в Balloon CRM!</h2>' +
'<p>Ваша персональная изолированная база данных успешно сгенерирована.</p>' +
'<p style="margin:20px 0;"><a href="' + tableUrl + '" target="_blank" style="display:inline-block;background:#28a745;color:#fff;padding:12px 24px;text-decoration:none;border-radius:4px;font-weight:bold;">Открыть таблицу данных</a></p>' +
'<hr style="border:0;border-top:1px solid #eee;margin:20px 0;">' +
'<p><b>Инструкция по использованию:</b><br>' +
'Перед началом работы обязательно ознакомьтесь с <a href="https://drive.google.com/drive/folders/1Ka4AVjId-mrEYWvOzuAMjbkOc6d52O53?dmr=1&ec=wgc-drive-%5Bmodule%5D-goto" target="_blank"> инструкцией по ссылке</a>.</p>' +
// 🔥 ИСПРАВЛЕННЫЙ SaaS-БЛОК КНОПКИ ВХОДА В CRM v50.0
'<div style="text-align: center; margin: 25px 0;">' +
'<a href="https://tvoymarketing.ru/kalkulator" target="_blank" style="display: inline-block; background-color: #1a73e8; color: #ffffff; font-family: \'Helvetica Neue\', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 700; text-decoration: none; padding: 12px 28px; border-radius: 6px; box-shadow: 0 2px 4px rgba(26,115,232,0.25); transition: background-color 0.2s;">' +
'Войти в личный кабинет CRM' +
'</a>' +
'</div>' +
'<div style="background-color:#f8f9fa;padding:15px;border-left:4px solid #1a73e8;margin:15px 0;border-radius:4px;">' +
'<b>Ваш логин:</b> ' + email + '<br>' +
'<b>Ваш пароль:</b> ' + password + '<br>' +
'<b>Пробный период:</b> 7 дней (до ' + formattedExpDate + ')' +
'</div>' +
'<p><b>Видеоматериалы:</b><br>' +
'Прикладываем видеоинструкции по настройке базы данных и обзор СРМ. Все видео доступны в облачной папке:</p>' +
'<p style="margin:20px 0;"><a href="https://drive.google.com/drive/folders/1Ka4AVjId-mrEYWvOzuAMjbkOc6d52O53?dmr=1&ec=wgc-drive-%5Bmodule%5D-goto" target="_blank" style="display:inline-block;background:#007bff;color:#fff;padding:12px 24px;text-decoration:none;border-radius:4px;font-weight:bold;">Смотреть видеоинструкции</a></p>' +
'</div>';
// Отправка под вашими железными правами Владельца
GmailApp.sendEmail(email, "Доступы и активация базы Balloon CRM", "", {
htmlBody: emailBody,
name: "Balloon CRM SaaS Platform"
});
// Меняем статус на SENT, чтобы не отправлять письмо повторно
controlSheet.getRange(i + 1, 9).setValue("SENT");
SpreadsheetApp.flush();
} catch(eMailErr) {
Logger.log("Ошибка отправки триггером для " + email + ": " + eMailErr.toString());
controlSheet.getRange(i + 1, 9).setValue("ERROR: " + eMailErr.toString());
}
}
}
}
function getFormattedAdminNewsHtml() {
return '<div style="font-family: Arial, sans-serif; line-height: 1.6; color: #333333; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e0e0e0; border-radius: 8px;">' +
' <h2 style="color: #1a73e8; margin-top: 0;">🎈 Balloon CRM: Большое обновление платформы и помощь в настройке!</h2>' +
' <p>Здравствуйте!</p>' +
' <p>Ваша подписка скоро закончится, для продления напишите создателю СРМ</p>' +
' <p>Мы упростили работу в СРМ и обновили инструкции, больше ни каких смежных таблиц, все в одной системе!</p>' +
' <div style="background-color: #e8f0fe; padding: 15px; border-radius: 6px; margin: 20px 0; border: 1px solid #b3d7ff;">' +
' <h3 style="color: #1a73e8; margin-top: 0; margin-bottom: 8px;">🔥 Главная новость: Полный отказ от таблиц!</h3>' +
' <p style="margin: 0; font-size: 14px;">Работать стало проще! <strong>с 16 июня 2026 года</strong>, мы выкатили масштабное обновление нашей первой версии. Вы сможете наслаждаться полноценным взаимодействием без необходимости перехода в таблицы данных — все возможности будут доступны в одном едином, удобном интерфейсе CRM со встроенными пошаговыми инструкциями.</p>' +
' </div>' +
' <p>Чтобы вы могли быстрее разобраться со всеми фишками и начать экономить время на рутине, мы открыли для вас <strong>прямую линию поддержки</strong>. Вы можете напрямую обратиться к создателю Balloon CRM в Telegram. Мы поможем адаптировать систему под ваши задачи, ответим на любые вопросы или проведем короткую демонстрацию.</p>' +
' <p style="background-color: #f8f9fa; padding: 12px; border-left: 4px solid #1a73e8; font-style: italic; font-size: 14px;">Это полностью бесплатно — нам искренне важно, чтобы наш продукт приносил реальную пользу вашей студии!</p>' +
'<div style="text-align: center; margin: 25px 0;">' +
'<a href="https://tvoymarketing.ru/kalkulator" target="_blank" style="display: inline-block; background-color: #1a73e8; color: #ffffff; font-family: \'Helvetica Neue\', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 700; text-decoration: none; padding: 12px 28px; border-radius: 6px; box-shadow: 0 2px 4px rgba(26,115,232,0.25); transition: background-color 0.2s;">' +
'Войти в личный кабинет CRM' +
'</a>' +
'</div>' +
' <p>Для связи просто нажмите на синюю кнопку ниже:</p>' +
' <div style="text-align: center; margin: 25px 0;">' +
' <a href="https://t.me/k_andrey_sv" target="_blank" style="background-color: #1a73e8; color: #ffffff; text-decoration: none; padding: 12px 30px; border-radius: 6px; font-weight: bold; display: inline-block; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">💬 Написать создателю в Telegram</a>' +
' </div>' +
' <p>Мы на связи и готовы помочь вашему бизнесу расти!</p>' +
' <hr style="border: 0; border-top: 1px solid #e0e0e0; margin: 20px 0;">' +
' <p style="font-size: 12px; color: #777777;">С уважением,<br>Команда облачной платформы Balloon CRM</p>' +
'</div>';
}
function smartCustomerUpsert(targetSheet, name, rawPhone, isFromOrder) {
try {
if (!rawPhone || !targetSheet) return "no_data";
// Сквозной поиск строго по последним 10 цифрам номера (игнорируем 7/8/+)
var digits = String(rawPhone).replace(/\D/g, "");
if (digits.length < 10) return "phone_too_short";
var cleanInputPhone = digits.substring(digits.length - 10);
var lastRow = targetSheet.getLastRow();
if (lastRow > 1) {
// Считываем только первые 3 колонки (A - ID, B - Телефон, C - Имя)
var data = targetSheet.getRange(2, 1, lastRow - 1, 3).getValues();
for (var i = 0; i < data.length; i++) {
var row = data[i];
if (!row || row.length < 2) continue;
var cellPhoneRaw = row[1] ? String(row[1]).replace(/\D/g, "") : "";
if (cellPhoneRaw.length < 10) continue;
var cleanBasePhone = cellPhoneRaw.substring(cellPhoneRaw.length - 10);
// 🔥 ЕСЛИ КЛИЕНТ НАЙДЕН -> МЕРЖИМ БЕЗ ИЗМЕНЕНИЯ ЕГО СОБЫТИЯ/ПРАЗДНИКА
if (cleanBasePhone === cleanInputPhone) {
var rowNum = i + 2;
// Обновляем только имя (Колонка C), если новое имя полнее
var currentNameInBase = row[2] ? String(row[2]).trim() : "";
if (name && (!currentNameInBase || String(name).length > currentNameInBase.length)) {
targetSheet.getRange(rowNum, 3).setValue(name);
}
// Обновляем дату отгрузки в формате YYYY-MM-DD при повторном обращении
var tildaFormattedDate = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "yyyy-MM-dd");
targetSheet.getRange(rowNum, 4).setValue(tildaFormattedDate); // Запись в колонку D
// Текст праздника менеджера в колонке E НЕ ТРОГАЕМ И НЕ СТИРАЕМ!
return "merged"; // Выходим, дубликат заблокирован!
}
}
}
// 🔥 СИНХРОНИЗАЦИЯ ID С TILDA: Берем последние 7 цифр номера телефона!
var phoneTailForId = cleanInputPhone.substring(cleanInputPhone.length - 7);
var newId = "C-" + phoneTailForId;
// ИСПРАВЛЕНО: Форматируем дату в понятный для Tilda вид "YYYY-MM-DD"
var tildaFormattedDate = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "yyyy-MM-dd");
targetSheet.appendRow([
newId, // А: ID Клиента
"7" + cleanInputPhone, // B: Телефон
name || "Без имени", // C: Имя
tildaFormattedDate, // D: Дата взаимодействия (Теперь отобразится!)
isFromOrder ? "Заказ шаров" : "Контакты", // E: Событие
isFromOrder ? "Из заказа" : "Внесено вручную", // F: Примечание
"Система" // G: Менеджер
]);
return "created";
} catch (internalErr) {
Logger.log("Ошибка в smartCustomerUpsert: " + internalErr.toString());
return "error";
}
}
// ✅ МЕЖДУНАРОДНАЯ НОРМАЛИЗАЦИЯ НОМЕРОВ НА СЕРВЕРЕ (E.164)
function normalizeServerPhone(rawPhone) {
if (rawPhone === null || rawPhone === undefined) return "";
// Принудительно превращаем любой тип данных в строку и очищаем от пробелов
var str = String(rawPhone).trim();
if (!str) return "";
var hasPlus = str.startsWith("+");
// Оставляем строго только цифры
var clean = str.replace(/\D/g, "");
if (!clean) return "";
if (hasPlus) return "+" + clean;
if (clean.length === 11 && (clean.startsWith("7") || clean.startsWith("8"))) {
return "+7" + clean.substring(1);
}
if (clean.length === 10) {
return "+7" + clean;
}
return "+" + clean;
}
/**
* 💾 СИНХРОНИЗАЦИЯ СТРУКТУРЫ ТОВАРОВ ИЗ CRM v100.0 (ПОЛНАЯ БЕЗОПАСНАЯ СБОРКА)
* Исключает падение формул ВПР в калькуляторе и блокирует создание фантомных заказов
*/
function coreSavePrices(sheetId, clientCatalog) {
if (!sheetId || !clientCatalog) return "error_bad_data";
try {
var catalogArr = clientCatalog;
// Декодируем и парсим JSON-строку от Tilda
if (typeof clientCatalog === "string") {
var decodedString = clientCatalog;
try {
while (decodedString.indexOf('%') !== -1) {
decodedString = decodeURIComponent(decodedString);
}
} catch(eDecode) {}
catalogArr = JSON.parse(decodedString);
}
if (!Array.isArray(catalogArr)) return "error_not_array";
var ss = SpreadsheetApp.openById(sheetId);
var goodsSheet = ss.getSheetByName('Товары') || ss.getSheetByName('товары');
if (!goodsSheet) return "error_no_sheet";
// 🛡️ ЖЕСТКИЙ ФИКС ДЛЯ ОБХОДА ФАНТОМНЫХ ТРИГГЕРОВ СВЯЗАННЫХ ЛИСТОВ
// Вместо .clear() стираем только контент первых двух столбцов, сохраняя ячейки живыми.
// Это гарантирует, что ВПР в калькуляторе таблицы не упадет в секундную ошибку #REF!
var lastRow = goodsSheet.getLastRow();
if (lastRow > 0) {
goodsSheet.getRange(1, 1, lastRow, 2).clearContent();
}
var rowsToPush = [["", ""]]; // Фиксированная пустая строка A1 под шапку калькулятора
var yellowRows = []; // Индексы строк для желтой подсветки категорий
catalogArr.forEach(function(item) {
if (!item || !item.name) return;
var colA = String(item.name).trim();
// 1. Сверяем булевый или текстовый флаг группы
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]);
}
});
// 2. Записываем плоскую таблицу строго в 2 столбца (A и B) поверх ячеек за один клик
var targetRange = goodsSheet.getRange(1, 1, rowsToPush.length, 2);
targetRange.setValues(rowsToPush);
// 3. Мягко сбрасываем старые стили в белый цвет в пределах 2 столбцов
if (lastRow > 0) {
goodsSheet.getRange(1, 1, lastRow, 2).setBackground("#ffffff").setFontWeight("normal");
}
// 4. Накладываем красивую желтую подсветку групп заново на 2 столбца
yellowRows.forEach(function(rowIndex) {
goodsSheet.getRange(rowIndex, 1, 1, 2).setBackground("#ffff00").setFontWeight("bold");
});
SpreadsheetApp.flush(); // Принудительно сбрасываем кэш Google V8 Engine
return "success";
} catch(eSavePricesErr) {
return "error: " + eSavePricesErr.toString();
}
}
function coreGetPrices(sheetId) {
var result = [];
if (!sheetId) return result;
try {
// 1. Подключаемся к таблице по её жесткому sheetId
var ss = SpreadsheetApp.openById(sheetId);
var goodsSheet = ss.getSheetByName('Товары') || ss.getSheetByName('товары');
if (!goodsSheet) return result;
// 2. Считываем ровно 2 столбца (Название и Цена)
var lastRow = goodsSheet.getLastRow();
var goods = [];
if (lastRow > 0) {
goods = goodsSheet.getRange(1, 1, lastRow, 2).getValues();
}
var currentGroup = "Каталог товаров";
for (var i = 0; i < goods.length; i++) {
var row = goods[i];
if (!row) continue;
var colA = String(row[0] || '').trim();
var rawPrice = row[1]; // Забираем исходное значение цены напрямую из столбца B
if (colA === "" && (rawPrice === "" || rawPrice === undefined || rawPrice === null)) continue;
// 🎯 СЦЕНАРИЙ А: НАЗВАНИЕ ЕСТЬ, ЦЕНЫ НЕТ — ЭТО ЗАГОЛОВОК ГРУППЫ
if (colA !== "" && (rawPrice === "" || rawPrice === undefined || rawPrice === null)) {
currentGroup = colA.replace(/[🎈✨🛍️🎁📂📁]/g, '').trim();
result.push({
name: colA,
price: "",
currentGroup: currentGroup,
group: currentGroup,
isGroup: true
});
continue;
}
// 🎯 СЦЕНАРИЙ Б: ЕСТЬ И НАЗВАНИЕ, И ЦЕНА — ЭТО ТОВАР С ЧЕСТНЫМИ ДРОБЯМИ (0.5)
if (colA !== "" && rawPrice !== "" && rawPrice !== undefined && rawPrice !== null) {
var parsedPrice = 0;
// ЗАЩИТА ТИПОВ ДАННЫХ V8:
if (typeof rawPrice === 'number') {
// Если Google Sheets уже хранит число (0.5), забираем его напрямую
parsedPrice = rawPrice;
} else {
// Если пришла строка (например "0,5"), вычищаем пробелы и меняем запятую на точку
var cleanStr = String(rawPrice).replace(/\s/g, '').replace(/[^0-9.,]/g, '').replace(',', '.');
parsedPrice = parseFloat(cleanStr) || 0;
}
result.push({
name: colA,
price: parsedPrice, // Гарантированно передаст 0.5 в Tilda
currentGroup: currentGroup,
group: currentGroup,
isGroup: false
});
}
}
} catch(ePricesLog) {
Logger.log("Ошибка выгрузки прайса в coreGetPrices: " + ePricesLog.toString());
}
return result;
}