var MASTER_CONTROL_ID = '15lc3jj5D4eadrrx0amG-rOE0yCPo2D1P51Hu-T-aV0c'; // Главный Master Control управления
function showSidebar() {
var html = HtmlService.createHtmlOutputFromFile('Sidebar').setTitle('Калькулятор').setWidth(350);
SpreadsheetApp.getUi().showSidebar(html);
}
function doGet(e) {
var callback = e.parameter.callback || 'callback';
var act = e.parameter.action || e.parameter.act || 'refresh';
var LOCAL_MASTER_ID = '15lc3jj5D4eadrrx0amG-rOE0yCPo2D1P51Hu-T-aV0c';
// 📦 ПЕРЕХВАТ ОБНОВЛЕНИЯ КАТАЛОГА ТОВАРОВ ЧЕРЕЗ GET (JSONP)
if (e && e.parameter && e.parameter.action === "save_catalog") {
// Берем sid строго из параметров GET-запроса Tilda
var targetSid = e.parameter.sid;
var resCode = coreSavePrices(targetSid, e.parameter.catalog);
// Используем уже объявленную на строке 15 переменную callback (БЕЗ ключевого слова var!)
var outText = (callback && callback !== 'callback') ? callback + "(" + JSON.stringify({status: resCode}) + ")" : JSON.stringify({status: resCode});
var outMime = (callback && callback !== 'callback') ? ContentService.MimeType.JAVASCRIPT : ContentService.MimeType.JSON;
return ContentService.createTextOutput(outText).setMimeType(outMime);
}
// 🟢 ПЕРЕХВАТ JSONP-ЗАПРОСА БЫСТРОГО СОХРАНЕНИЯ КЛИЕНТОВ С ТИЛЬДЫ v50.0
if (e.parameter.act === 'save_fast_client' || e.parameter.action === 'save_fast_client' || act === 'save_fast_client') {
try {
// 🤖 АВТОМАТИЧЕСКИЙ РОБОТ СКВОЗНОЙ БАЗЫ КЛИЕНТОВ ДЛЯ РУЧНОГО ВВОДА (ИСПРАВЛЕН)
// Теперь этот блок срабатывает ТОЛЬКО при ручном добавлении контакта менеджером
var currentAct = e.parameter.act || e.parameter.action || (typeof act !== 'undefined' ? act : "");
if (currentAct === 'save_fast_client' || currentAct === 'add_client') {
try {
var rawPhoneFromManual = e.parameter.phone || e.parameter.clientPhone || data.phone || "";
var cleanPhoneFromManual = String(rawPhoneFromManual).replace(/\D/g, "");
if (cleanPhoneFromManual.length === 11 && cleanPhoneFromManual.indexOf('8') === 0) {
cleanPhoneFromManual = "7" + cleanPhoneFromManual.substring(1);
}
if (cleanPhoneFromManual) {
var autoClientSheet = ss.getSheetByName("база_клиентов");
if (!autoClientSheet) {
autoClientSheet = ss.insertSheet("база_клиентов");
autoClientSheet.appendRow(["ID Клиента", "Телефон", "Имя", "Дата контакта", "Событие", "Примечание", "Менеджер"]);
}
var manualClientName = e.parameter.name || e.parameter.clientName || data.name || "Новый контакт";
// Вызываем наш безопасный цифровой апсерт (isFromOrder = false, так как ввод ручной)
smartCustomerUpsert(autoClientSheet, manualClientName, cleanPhoneFromManual, false);
SpreadsheetApp.flush();
}
} catch (autoClientErr) {
Logger.log("Заминка ручной регистрации клиента в CRM: " + autoClientErr.toString());
}
}
var sId = e.parameter.sheetId;
if (!sId) {
return ContentService.createTextOutput(callback + "(" + JSON.stringify({result:"error", error:"Не передан ID таблицы sheetId"}) + ");").setMimeType(ContentService.MimeType.JAVASCRIPT);
}
var ss = SpreadsheetApp.openById(sId);
var targetPhone = normalizeServerPhone(e.parameter.clientPhone);
var clientSheet = ss.getSheetByName("база_клиентов");
if (!clientSheet) {
clientSheet = ss.insertSheet("база_клиентов");
clientSheet.appendRow(["ID Клиента", "Телефон", "Имя", "Дата контакта", "Событие", "Примечание", "Менеджер"]);
}
var cData = clientSheet.getDataRange().getValues();
var foundRowIdx = -1;
if (cData && cData.length > 1) {
for (var i = 1; i < cData.length; i++) {
if (!cData[i] || cData[i].length < 2) continue;
var cellPhone = cData[i][1] ? cData[i][1].toString().trim() : ""; // Индекс 1 — это колонка телефона
if (!cellPhone) continue;
if (normalizeServerPhone(cellPhone) === targetPhone) {
foundRowIdx = i + 1;
break;
}
}
}
var formattedDate = e.parameter.clientDate ? new Date(e.parameter.clientDate) : new Date();
if (isNaN(formattedDate.getTime())) formattedDate = new Date();
if (foundRowIdx === -1) {
var newId = "C" + Math.floor(100000 + Math.random() * 900000);
clientSheet.appendRow([
newId,
targetPhone,
e.parameter.clientName || "Без имени",
formattedDate,
e.parameter.clientEvent || "",
e.parameter.comment || "",
e.parameter.manager || "CRM"
]);
} else {
clientSheet.getRange(foundRowIdx, 3).setValue(e.parameter.clientName || "Без имени");
clientSheet.getRange(foundRowIdx, 4).setValue(formattedDate);
clientSheet.getRange(foundRowIdx, 5).setValue(e.parameter.clientEvent || "");
clientSheet.getRange(foundRowIdx, 6).setValue(e.parameter.comment || "");
}
SpreadsheetApp.flush();
// 🚀 Возвращаем JSONP-сигнал, который мгновенно перезагрузит страницу на Tilda
return ContentService.createTextOutput(callback + "(" + JSON.stringify({result: "success"}) + ");")
.setMimeType(ContentService.MimeType.JAVASCRIPT);
} catch (err) {
return ContentService.createTextOutput(callback + "(" + JSON.stringify({result: "error", error: "Сбой Ядра в doGet: " + err.toString()}) + ");")
.setMimeType(ContentService.MimeType.JAVASCRIPT);
}
}
// 🗑️ ПЕРЕХВАТ JSONP-ЗАПРОСА НА УДАЛЕНИЕ КЛИЕНТА v50.0
if (act === 'delete_customer_contact' || act === 'delete_fast_client') {
try {
var sId = e.parameter.sheetId;
if (!sId) return ContentService.createTextOutput(callback + "(" + JSON.stringify({result:"error", error:"Не передан ID таблицы"}) + ");").setMimeType(ContentService.MimeType.JAVASCRIPT);
var ss = SpreadsheetApp.openById(sId);
var targetPhone = normalizeServerPhone(e.parameter.clientPhone);
var clientSheet = ss.getSheetByName("база_клиентов");
if (clientSheet) {
var cData = clientSheet.getDataRange().getValues();
if (cData && cData.length > 1) {
for (var i = 1; i < cData.length; i++) {
if (!cData[i] || cData[i].length < 2) continue;
var cellPhone = cData[i] ? cData[i].toString().trim() : "";
// Если нашли строку с этим телефоном — физически удаляем её из таблицы
if (normalizeServerPhone(cellPhone) === targetPhone) {
clientSheet.deleteRow(i + 1);
break;
}
}
}
}
SpreadsheetApp.flush();
return ContentService.createTextOutput(callback + "(" + JSON.stringify({result: "success"}) + ");")
.setMimeType(ContentService.MimeType.JAVASCRIPT);
} catch (err) {
return ContentService.createTextOutput(callback + "(" + JSON.stringify({result: "error", error: "Сбой удаления в Ядре: " + err.toString()}) + ");")
.setMimeType(ContentService.MimeType.JAVASCRIPT);
}
}
try {
var controlSheet = SpreadsheetApp.openById(LOCAL_MASTER_ID).getSheetByName('Пользователи');
var usersRange = controlSheet.getDataRange().getValues();
// 1. СЦЕНАРИЙ АВТОРИЗАЦИИ (LOGIN)
if (act === 'login') {
var inputLogin = String(e.parameter.login || '').trim().toLowerCase();
var inputPass = String(e.parameter.pass || '').trim();
for (var i = 1; i < usersRange.length; i++) {
var row = usersRange[i]; // ◄ ПЕРЕМЕННАЯ ТЕКУЩЕЙ СТРОКИ СВОБОДНА И ДОСТУПНА ТЕПЕРЬ ВСЕМ ФУНКЦИЯМ
if (!row || row.length < 3) continue;
// Сверяем логин и пароль по точным индексам колонок вашей таблицы (A и B)
if (String(row[0]).trim().toLowerCase() === inputLogin && String(row[1]).trim() === inputPass) {
// 🛡️ ЖЕСТКИЙ ВАЛИДАТОР СРОКА ПОДПИСКИ (СТОЛБЕЦ G = row[6])
var rawExpiry = row[6];
if (rawExpiry) {
var expiryDate = new Date(rawExpiry);
var today = new Date();
expiryDate.setHours(0,0,0,0);
today.setHours(0,0,0,0);
// Резервный парсинг формата DD.MM.YYYY, если Google прочитал ячейку как обычный текст
if (isNaN(expiryDate.getTime())) {
var parts = String(rawExpiry).split('.');
if (parts.length === 3) {
expiryDate = new Date(parseInt(parts[2]), parseInt(parts[1]) - 1, parseInt(parts[0]));
expiryDate.setHours(0,0,0,0);
}
}
// Если дата окончания подписки меньше сегодняшнего дня — жестко блокируем вход!
if (!isNaN(expiryDate.getTime()) && expiryDate < today) {
return ContentService.createTextOutput(callback + "(" + JSON.stringify({ success: false, error: "subscription_expired" }) + ")").setMimeType(ContentService.MimeType.JAVASCRIPT);
}
}
// Если подписка активна — собираем штатный payload для Tilda
var successPayload = {
success: true,
role: String(row[4] || 'staff').trim().toLowerCase(), // Колонка E
login: inputLogin, // Колонка A
sheetId: String(row[2]).trim(), // Колонка C
compName: String(row[3]).trim() // Colony D
};
return ContentService.createTextOutput(callback + "(" + JSON.stringify(successPayload) + ")").setMimeType(ContentService.MimeType.JAVASCRIPT);
}
}
return ContentService.createTextOutput(callback + "(" + JSON.stringify({ success: false, error: "wrong_credentials" }) + ")").setMimeType(ContentService.MimeType.JAVASCRIPT);
}
// ======= БЕЗОПАСНЫЙ ДОП ПЛАГИН: ПОЛУЧЕНИЕ НАСТРОЕК ДЛЯ ТИЛЬДЫ =======
else if (e.parameter.type === 'get_settings') {
try {
var currentSheetId = e.parameter.sheet_id;
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
};
var callback = e.parameter.callback || "callback";
return ContentService.createTextOutput(callback + "(" + JSON.stringify(result) + ");")
.setMimeType(ContentService.MimeType.JAVASCRIPT);
} catch(eGet) {
var errCallback = e.parameter.callback || "callback";
return ContentService.createTextOutput(errCallback + "({status:'error'});")
.setMimeType(ContentService.MimeType.JAVASCRIPT);
}
}
// ====================================================================
// 2. СЦЕНАРИЙ СИНХРОНИЗАЦИИ И СБОР ДАННЫХ ДЛЯ ТИЛЬДЫ
var sId = e.parameter.id || e.parameter.sheetId || '';
var loginOwner = e.parameter.owner || e.parameter.login || '';
// 🔥 БРОНЕБОЙНАЯ ЗАЩИТА: Если Tilda прислала пустой ID таблицы,
// но передала логин (как на скриншоте из сети), Ядро САМО найдет нужный sheetId в Мастере!
if (sId === '' && loginOwner !== '') {
var cleanLogin = String(loginOwner).trim().toLowerCase();
for (var k = 1; k < usersRange.length; k++) {
if (usersRange[k] && String(usersRange[k][0]).trim().toLowerCase() === cleanLogin) {
sId = String(usersRange[k][2]).trim(); // Автоматически вытаскиваем ID таблицы из колонки C
break;
}
}
}
// 🛡️ СИСТЕМНЫЙ БЛОКИРАТОР СЕССИЙ В GET-ЗАПРОСЕ v15.5 (ТОЧНЫЕ ИНДЕКСЫ)
if (sId && sId !== "") {
try {
for (var k = 1; k < usersRange.length; k++) {
if (usersRange[k] && usersRange[k][2]) {
// 🔥 ТОЧНЫЙ СБОР: Сверяем ID таблицы со столбцом C (индекс 2)
if (String(usersRange[k][2]).trim() === String(sId).trim()) {
// 🔥 ТОЧНЫЙ СБОР: Берём дату окончания из столбца G (индекс 6)
var gExpiry = usersRange[k][6];
if (gExpiry) {
var gExpiryDate = new Date(gExpiry);
var gToday = new Date();
gExpiryDate.setHours(0,0,0,0);
gToday.setHours(0,0,0,0);
if (isNaN(gExpiryDate.getTime())) {
var gParts = String(gExpiry).split('.');
if (gParts.length === 3) {
gExpiryDate = new Date(parseInt(gParts[2]), parseInt(gParts[1]) - 1, parseInt(gParts[0]));
gExpiryDate.setHours(0,0,0,0);
}
}
// Если дата окончания меньше сегодняшнего дня — полностью блокируем выдачу базы!
if (!isNaN(gExpiryDate.getTime()) && gExpiryDate.getTime() < gToday.getTime()) {
var errCallback = e.parameter.callback || 'callback';
return ContentService.createTextOutput(errCallback + "(" + JSON.stringify({ success: false, error: "subscription_expired" }) + ")")
.setMimeType(ContentService.MimeType.JAVASCRIPT);
}
}
break;
}
}
}
} catch(eGetAuthErr) {}
}
// Если даже после этого ID пуст — отдаем пустой пакет с ошибкой, чтобы не вешать сервер Tilda
if (!sId) {
return ContentService.createTextOutput(callback + "(" + JSON.stringify({ catalog: [], deals: [], expenses: [], payments: [], team: [], statuses: [], clients: [], error: "ID таблицы не найден" }) + ")").setMimeType(ContentService.MimeType.JAVASCRIPT);
}
// Если старая библиотека запрашивает инициализацию каталога товаров
if (act === 'getSaaSAppInitPack' || e.parameter.method === 'getSaaSAppInitPack') {
var ss = SpreadsheetApp.openById(sId);
var catalogSheet = ss.getSheetByName('Товары');
var catalogData = catalogSheet ? catalogSheet.getDataRange().getValues() : [];
return ContentService.createTextOutput(callback + "(" + JSON.stringify({ sheetId: sId, catalog: catalogData, success: true }) + ")").setMimeType(ContentService.MimeType.JAVASCRIPT);
}
var dataPackage = fetchCRMData(sId, loginOwner);
var saasChannelsList = [];
try {
var chanSs = SpreadsheetApp.openById(sId);
var chanSheet = chanSs.getSheetByName('Каналы') || chanSs.getSheetByName('каналы');
if (chanSheet) {
var lastChanRow = chanSheet.getLastRow();
if (lastChanRow > 1) {
// Считываем строго колонку А со 2-й строки (исключая шапку "Источник")
var chanValues = chanSheet.getRange(2, 1, lastChanRow - 1, 1).getValues();
for (var c = 0; c < chanValues.length; c++) {
var chanName = String(chanValues[c][0]).trim(); // [0] обеспечивает чтение ячейки
if (chanName) saasChannelsList.push(chanName);
}
}
}
} catch(eChan) {
Logger.log("Ошибка парсинга листа Каналы: " + eChan.toString());
}
// Резервный список на случай, если у кого-то лист "Каналы" действительно будет пуст
if (!saasChannelsList.length) {
saasChannelsList = ["ВКонтакте", "WhatsApp", "Instagram", "Повторный"];
}
// Дописываем массивы каналов прямо внутрь нашего единого пакета данных CRM
dataPackage.channels = saasChannelsList;
dataPackage.marketing_channels = saasChannelsList;
// Отправляем укомплектованный пакет обратно на Tilda
return ContentService.createTextOutput(callback + "(" + JSON.stringify(dataPackage) + ")").setMimeType(ContentService.MimeType.JAVASCRIPT);
} catch(err) {
// Возвращаем текст системной ошибки на Tilda для моментальной диагностики
return ContentService.createTextOutput(callback + "(" + JSON.stringify({ success: false, error: "Бэкенд сбой: " + err.toString() }) + ")").setMimeType(ContentService.MimeType.JAVASCRIPT);
}
}
function res(c, o) {
return ContentService.createTextOutput(c + "(" + JSON.stringify(o) + ")").setMimeType(ContentService.MimeType.JAVASCRIPT);
}
function fetchCRMData(sId, ownerLogin) {
var package = { catalog: [], deals: [], expenses: [], payments: [], team: [], statuses: [], clients: [] };
if (!sId) return package;
var clientMap = {};
try {
var ss = SpreadsheetApp.openById(sId);
// 🔥 ПРИНУДИТЕЛЬНЫЙ СБРОС КЭША ДЛЯ СТАРЫХ И НОВЫХ ТАБЛИЦ:
SpreadsheetApp.flush();
Utilities.sleep(100);
var dealsSheet = ss.getSheetByName('база_crm') || ss.insertSheet('база_crm');
var paySheet = ss.getSheetByName('реестр_платежей') || ss.insertSheet('реестр_платежей');
var expSheet = ss.getSheetByName('расходы') || ss.insertSheet('расходы');
var goodsSheet = ss.getSheetByName('Товары') || ss.getSheetByName('каталог') || ss.getSheetByName('Каталог');
var deals = dealsSheet.getDataRange().getDisplayValues();
var pay = paySheet.getDataRange().getValues();
var exp = expSheet.getDataRange().getValues();
var clientSheet = ss.getSheetByName("база_клиентов");
if (!clientSheet) {
clientSheet = ss.insertSheet("база_клиентов");
clientSheet.appendRow(["ID Клиента", "Телефон", "Имя", "Дата контакта", "Событие", "Примечание", "Менеджер"]);
}
var manualClientsRaw = clientSheet.getDataRange().getValues();
if (manualClientsRaw && manualClientsRaw.length > 1) {
for (var m = 1; m < manualClientsRaw.length; m++) {
var mRow = manualClientsRaw[m];
if (!mRow || mRow.length < 2) continue;
var rawPhoneStr = mRow[1] ? mRow[1].toString().trim() : "";
if (!rawPhoneStr) continue; // Жесткий пропуск пустых строк в Excel
var normPhone = normalizeServerPhone(rawPhoneStr);
var currentClientId = mRow[0] ? String(mRow[0]).trim() : "";
if (!currentClientId) {
currentClientId = "C" + Math.floor(100000 + Math.random() * 900000);
clientSheet.getRange(m + 1, 1).setValue(currentClientId);
}
var rawDate = mRow[3];
var isoDateText = "";
if (rawDate instanceof Date) {
isoDateText = Utilities.formatDate(rawDate, "GMT+3", "yyyy-MM-dd");
} else if (rawDate) {
try { isoDateText = convertTextDate(rawDate); } catch(e){}
}
clientMap[normPhone] = {
id: currentClientId,
name: mRow[2] ? String(mRow[2]).trim() : "Без имени",
phone: normPhone,
buy_count: 0,
successful_count: 0,
total_spent: 0,
last_date: isoDateText,
last_event: mRow[4] ? String(mRow[4]).trim() : "Контакты",
comment: mRow[5] ? String(mRow[5]).trim() : "",
manager: mRow[6] ? String(mRow[6]).trim() : "CRM",
order_history: [{
is_manual: true,
type: 'manual',
date: isoDateText || 'не указана',
event: mRow[4] ? String(mRow[4]).trim() : "Внесение контакта",
products: mRow[5] ? String(mRow[5]).trim() : "Без примечания"
}]
};
}
}
// Сбор и автоматическая генерация матрицы правил статусов воронки из Excel
var statusSheet = ss.getSheetByName('Статусы') || ss.insertSheet('Статусы');
if (statusSheet.getLastRow() === 0) {
statusSheet.appendRow(["Название", "В реализации", "В оборот", "В дебиторку"]);
statusSheet.appendRow(["Новое обращение", "НЕТ", "НЕТ", "НЕТ"]);
statusSheet.appendRow(["Общение", "НЕТ", "НЕТ", "НЕТ"]);
statusSheet.appendRow(["Согласован", "ДА", "ДА", "ДА"]);
statusSheet.appendRow(["Внесена оплата", "ДА", "ДА", "ДА"]);
statusSheet.appendRow(["Готов", "ДА", "ДА", "ДА"]);
statusSheet.appendRow(["Выполнен", "НЕТ", "ДА", "ДА"]); // 🎯 Исправлено по скриншоту клиента
statusSheet.appendRow(["Отказ", "НЕТ", "НЕТ", "НЕТ"]);
statusSheet.appendRow(["Не целевой", "НЕТ", "НЕТ", "НЕТ"]);
}
var sData = statusSheet.getDataRange().getValues();
for (var i = 1; i < sData.length; i++) {
if (sData[i] && sData[i][0] && String(sData[i][0]).trim() !== "") {
var sNameClean = String(sData[i][0]).trim();
var sNameLower = sNameClean.toLowerCase();
// Жесткие дефолтные правила на случай, если ячейки в Excel случайно затерли руками
var defRealization = (['согласован', 'внесена оплата', 'готов'].indexOf(sNameLower) !== -1) ? "ДА" : "НЕТ";
var defRevenue = (['согласован', 'внесена оплата', 'готов', 'выполнен'].indexOf(sNameLower) !== -1) ? "ДА" : "НЕТ";
var defDebt = (['согласован', 'внесена оплата', 'готов', 'выполнен'].indexOf(sNameLower) !== -1) ? "ДА" : "НЕТ";
var valReal = sData[i][1] ? String(sData[i][1]).trim().toUpperCase() : defRealization;
var valRev = sData[i][2] ? String(sData[i][2]).trim().toUpperCase() : defRevenue;
var valDebt = sData[i][3] ? String(sData[i][3]).trim().toUpperCase() : defDebt;
package.statuses.push({
name: sNameClean,
isRealization: valReal === 'ДА' || valReal === 'YES',
isRevenue: valRev === 'ДА' || valRev === 'YES',
isDebt: valDebt === 'ДА' || valDebt === 'YES'
});
}
}
// Пакетный сбор всей команды, привязанной к текущему ID таблицы
try {
var controlSheet = SpreadsheetApp.openById(MASTER_CONTROL_ID).getSheetByName('Пользователи');
if (controlSheet) {
var uData = controlSheet.getDataRange().getValues();
for (var i = 1; i < uData.length; i++) {
var uRow = uData[i];
// Сопоставляем индекс 2 (колонка C с ID таблицы)
if (uRow && uRow.length >= 3 && String(uRow[2]).trim() === String(sId).trim()) {
package.team.push({
login: String(uRow[0] || '').trim(),
role: String(uRow[4] || 'staff').trim().toLowerCase()
});
}
}
}
} catch(e) { package.team = []; }
// Набиваем каталог товаров в ОЗУ фронтенда
var goods = [];
if (goodsSheet) {
var goodsData = goodsSheet.getDataRange().getValues();
for (var i = 1; i < goodsData.length; i++) {
if (!goodsData[i] || !goodsData[i][0]) continue;
var pName = String(goodsData[i][0]).trim(); var rawPrice = goodsData[i][1];
if (pName !== "") {
if (rawPrice === "" || rawPrice === null || rawPrice === undefined) { goods.push({ name: pName, price: "", isGroup: true }); }
else { goods.push({ name: pName, price: parseInt(rawPrice) || 0, isGroup: false }); }
}
}
}
package.catalog = goods;
// 2. ПАРСИНГ И СБОР ВСЕХ СДЕЛOК ИЗ ТАБЛИЦЫ КЛИЕНТА
for (var i = 1; i < deals.length; i++) {
var r = deals[i]; if (!r || !r[0] || String(r[0]).trim() === "") continue;
var dEStr = ""; try { dEStr = convertTextDate(r[6]); } catch(e){}
var dDStr = ""; try { dDStr = convertTextDate(r[7]); } catch(e){}
var dDateCreated = ""; try { dDateCreated = convertTextDate(r[1]); } catch(e){}
var dealObj = {
row_num: i + 1, row: i + 1, id: String(r[0]).trim(), date_created: dDateCreated, client_name: String(r[2] || ''), phone: String(r[3] || ''),
status: String(r[4] || 'Новое обращение').trim(), event_name: String(r[5] || ''), date_event: dEStr, date_delivery: dDStr,
time_delivery: String(r[8] || '').trim(), products: String(r[9] || ''), details: String(r[10] || '').trim(), address: String(r[11] || ''),
subtotal: Number(r[12] || 0), discount: r[13] !== undefined && r[13] !== null && String(r[13]).trim() !== "" ? String(r[13]) : "0%",
delivery: Number(r[14] || 0), total: Number(r[15] || 0), prepay: Number(r[16] || 0), leftPay: Number(r[17] || 0),
manager: r[18] ? String(r[18]).trim() : '', photo: r[19] ? String(r[19]).trim() : '', channel: r[20] ? String(r[20]).trim() : '',
badge: r[21] ? String(r[21]).trim() : '', calId: r[22] ? String(r[22]).trim() : ''
};
package.deals.push(dealObj);
var cleanPhone = dealObj.phone.replace(/\D/g, '');
if (cleanPhone) {
if (!clientMap[cleanPhone]) {
clientMap[cleanPhone] = {
name: dealObj.client_name, phone: dealObj.phone, cleanPhone: cleanPhone, buy_count: 0, successful_count: 0, total_spent: 0,
last_date: dealObj.date_delivery, order_history: []
};
}
var cProfile = clientMap[cleanPhone];
cProfile.buy_count++;
cProfile.order_history.push({ id: dealObj.id, date: dealObj.date_delivery, date_created: dDateCreated, status: dealObj.status, products: dealObj.products, total: dealObj.total });
}
}
package.deals.reverse(); // Новые заказы первыми в ленте Tilda
// 1. ВЫНОСИМ ФУНКЦИЮ ИЗ ЦИКЛА И ДОБАВЛЯЕМ ПРОВЕРКУ ТИПА ДАТЫ
function convertTextDate(rawDate) {
if (!rawDate) return "";
// 1. Если это системный объект даты Google, переводим в текст ISO
if (rawDate instanceof Date) {
try {
return Utilities.formatDate(rawDate, Session.getScriptTimeZone(), "yyyy-MM-dd");
} catch(e) { return ""; }
}
var str = String(rawDate).trim();
if (str === "") return "";
var day = "", month = "", year = "";
// 2. Если дата с точками (дд.мм.гггг), например: "07.06.2026 18:45:00"
if (str.indexOf('.') !== -1) {
var parts = str.split('.');
if (parts.length >= 3) {
day = parts[0].trim();
month = parts[1].trim();
year = parts[2].trim().substring(0, 4); // Берём строго первые 4 цифры года, отсекая время!
}
}
// 3. Если дата со слэшами (мм/дд/гггг или дд/мм/гггг), часто бывает при копировании
else if (str.indexOf('/') !== -1) {
var parts = str.split('/');
if (parts.length >= 3) {
// Проверяем, что идет первым (если первая часть длиннее 2 символов - это гггг-мм-дд)
if (parts[0].trim().length === 4) {
year = parts[0].trim();
month = parts[1].trim();
day = parts[2].trim().substring(0, 2);
} else {
day = parts[0].trim();
month = parts[1].trim();
year = parts[2].trim().substring(0, 4);
}
}
}
// 4. Если дата уже в формате ISO (гггг-мм-дд)
else if (str.indexOf('-') !== -1) {
var parts = str.split('-');
if (parts.length >= 3) {
year = parts[0].trim();
month = parts[1].trim();
day = parts[2].trim().substring(0, 2);
}
}
// Если не удалось разобрать компоненты, возвращаем как есть
if (!day || !month || !year) return str;
// Дописываем нули для корректной фильтрации на Тильде (чтобы было "07", а не "7")
if (day.length === 1) day = "0" + day;
if (month.length === 1) month = "0" + month;
return year + "-" + month + "-" + day; // Всегда возвращает идеальный гггг-мм-дд
}
// 3. Финальный расчет LTV и упаковка в JSON для отправки на Tilda v50.0
for (var k in clientMap) {
var cl = clientMap[k];
cl.order_history.forEach(function(o) {
if (!o.is_manual && String(o.status).toLowerCase().trim() === 'выполнен') {
cl.successful_count++;
cl.total_spent += o.total;
}
});
var activeCount = cl.buy_count;
var cRate = activeCount ? Math.round((cl.successful_count / activeCount) * 100) : 0;
cl.conversion_text = "Выполнено заказов: " + cl.successful_count + " из " + cl.buy_count + " | Конверсия: " + cRate + "%";
package.clients.push(cl);
}
package.payments = pay.slice(1).map(function(row) {
var pDate = ""; try { if(row[0] && row[0] instanceof Date) pDate = Utilities.formatDate(new Date(row[0]),"GMT+3","yyyy-MM-dd"); } catch(e){}
return { date: pDate, orderId: row[1] !== undefined ? String(row[1]) : '', sum: row[3] !== undefined ? Number(row[3]) : 0 };
});
package.expenses = exp.slice(1).map(function(row) {
var eDate = ""; try { if(row[0] && row[0] instanceof Date) eDate = Utilities.formatDate(new Date(row[0]),"GMT+3","yyyy-MM-dd"); } catch(e){}
return { date: eDate, title: row[1] !== undefined ? String(row[1]) : '', sum: row[2] !== undefined ? Number(row[2]) : 0 };
});
} catch(errMaster) { Logger.log("Ошибка пакетного сбора: " + errMaster.toString()); }
return package;
}
function doPost(e) {
// 📦 УЛЬТРА-ЗАЩИЩЕННЫЙ ПЕРЕХВАТЧИК КАТАЛОГА ТОВАРОВ v105.0 (ЖЕСТКИЙ ПАРСИНГ POST)
try {
var pAction = "";
var targetSid = "";
var rawCatalogData = "";
// 1. Попытка прочитать параметры из стандартного объекта Google
if (e && e.parameter) {
pAction = e.parameter.action || "";
targetSid = e.parameter.sheet_id || e.parameter.sid || "";
rawCatalogData = e.parameter.catalog || "";
}
// 2. ГЛАВНЫЙ ИСПРАВЛЕННЫЙ ШЛЮЗ ДЛЯ XHR (Разбираем сырой текст postData.contents)
if (e && e.postData && e.postData.contents && (!pAction || pAction === "")) {
var rawBody = e.postData.contents;
// Декодируем строку, если браузер обернул её несколько раз
try {
while (rawBody.indexOf('%') !== -1) { rawBody = decodeURIComponent(rawBody); }
} catch(eDecodeBody) {}
// Самостоятельно разбираем строку параметров x-www-form-urlencoded на ключи и значения
var pairs = rawBody.split('&');
var parsedParams = {};
for (var i = 0; i < pairs.length; i++) {
var pair = pairs[i].split('=');
var key = decodeURIComponent(pair[0] || "");
var value = decodeURIComponent(pair[1] || "");
if (key) { parsedParams[key] = value; }
}
pAction = parsedParams.action || "";
targetSid = parsedParams.sheet_id || parsedParams.sid || "";
rawCatalogData = parsedParams.catalog || "";
}
// 3. ПРОВЕРКА КОМАНДЫ СОХРАНЕНИЯ ЦЕН
if (pAction === "save_catalog" || pAction === "update_balloon_prices") {
var executeResult = "error_missing_sheet_id";
if (targetSid && targetSid !== "") {
// Запускаем физическую перезапись ячеек и желтую подсветку категорий
executeResult = coreSavePrices(targetSid, rawCatalogData);
// Логируем время в Настройки таблицы этого конкретного пользователя платформы
try {
SpreadsheetApp.openById(targetSid).getSheetByName('Настройки').getRange('H5').setValue('🚀 Прайс обновлен в: ' + new Date().toLocaleTimeString());
} catch(eTimeLog) {}
}
// КРИТИЧЕСКИЙ ДОСРОЧНЫЙ ВЫХОД: отдаем ответ на Tilda и НАМЕРТВО прерываем doPost!
// Скрипт создания пустых сделок на строке 688 физически не получит управления!
return ContentService.createTextOutput(JSON.stringify({ status: executeResult }))
.setMimeType(ContentService.MimeType.JSON)
.addHeader('Access-Control-Allow-Origin', '*')
.addHeader('Access-Control-Allow-Methods', 'POST');
}
} catch (eGlobalCatalogErr) {
// В случае форс-мажора тихо пропускаем трафик дальше к штатной логике движка
}
// === СТРОКА ЛОГИРОВАНИЯ ДЛЯ ОСТАЛЬНЫХ ФОРМ (ОСТАВИТЬ БЕЗ ИЗМЕНЕНИЙ) ===
try { SpreadsheetApp.openById(e.parameter.sheet_id || e.parameter.sid).getSheetByName('Настройки').getRange('H5').setValue('🚀 Запрос получен в: ' + new Date().toLocaleTimeString()); } catch(e){}
// 🔏 НАЧАЛО ВАШЕГО ОРИГИНАЛЬНОГО SaaS-КОДА ЗАКАЗОВ (СТРОКА 688):
var checkData = {};
try {
if (e && e.postData && e.postData.contents) { checkData = JSON.parse(e.postData.contents); }
} catch(err) {}
if (e && e.parameter) { for (var k in e.parameter) { checkData[k] = e.parameter[k]; } }
var checkAction = checkData.action || checkData.act;
// Если прилетел запрос на создание нового личного кабинета
if (checkAction === "register") {
return handleSaaSRegistration(checkData); // Уводим поток на авто-генерацию базы в самый низ файла
}
// 🔑 ПЕРЕХВАТ ВОССТАНОВЛЕНИЯ ПАРОЛЯ v50.0
if (checkAction === "forgot_password") {
var searchEmail = String(checkData.email || '').trim().toLowerCase();
var controlSheet = SpreadsheetApp.openById('15lc3jj5D4eadrrx0amG-rOE0yCPo2D1P51Hu-T-aV0c').getSheetByName('Пользователи');
var usersData = controlSheet.getDataRange().getValues();
var userRowIndex = -1;
// Ищем пользователя в Мастер-Таблице по Email
for (var i = 1; i < usersData.length; i++) {
if (String(usersData[i][0]).toLowerCase().trim() === searchEmail) {
userRowIndex = i + 1; // Запоминаем физический номер строки в Excel
break;
}
}
if (userRowIndex === -1) {
// Возвращаем чистый текст отказа, который Tilda сразу поймет
return ContentService.createTextOutput("user_not_found").setMimeType(ContentService.MimeType.TEXT);
}
// Записываем PENDING в столбец I (9-я колонка) для триггера отправки
controlSheet.getRange(userRowIndex, 9).setValue("PENDING");
SpreadsheetApp.flush();
// 🔥 ОФИЦИАЛЬНЫЙ ТЕКСТОВЫЙ ОТВЕТ ДЛЯ АВТОНОМНОЙ КНОПКИ TILDA
return ContentService.createTextOutput("success").setMimeType(ContentService.MimeType.TEXT);
}
// Если новые таблицы стучатся через сетевой мост меню (настройка Диска/Календаря)
if (checkAction === "setup_user_drive" || checkAction === "setup_user_calendar") {
return ContentService.createTextOutput("✅ Операция успешно зарегистрирована в Ядре.").setMimeType(ContentService.MimeType.TEXT);
}
// 📥 СЕТЕВАЯ ПЕРЕДАЧА ИНТЕРФЕЙСА КАЛЬКУЛЯТОРА ДЛЯ ТАБЛИЦ КЛИЕНТОВ v50.0
if (checkAction === "get_sidebar") {
try {
// Считываем ваш родной файл Sidebar.html (с большой буквы S, как на панели файлов!)
var htmlContent = HtmlService.createHtmlOutputFromFile('Sidebar').getContent();
return ContentService.createTextOutput(htmlContent).setMimeType(ContentService.MimeType.TEXT);
} catch(eSide) {
return ContentService.createTextOutput("❌ Ошибка Ядра при чтении калькулятора: " + eSide.toString()).setMimeType(ContentService.MimeType.TEXT);
}
}
// 📥 СЕТЕВОЙ ИМПОРТ ТОВАРОВ ИЗ YML-ВЫГРУЗКИ TILDA v50.0
if (checkAction === "update_tilda_export") {
try {
var targetSheetId = checkData.sheetId || checkData.sheet_id || checkData.id;
if (!targetSheetId) return ContentService.createTextOutput("error: Не передан ID таблицы").setMimeType(ContentService.MimeType.TEXT);
// Вызываем оригинальную функцию импорта, которая уже есть ниже в вашем файле Импорты.gs
var importResult = coreUpdateTildaExport(targetSheetId);
if (importResult && importResult.indexOf("added:") === 0) {
var parts = importResult.split(":");
var addedCount = parts[1] || "0";
return ContentService.createTextOutput("🎉 УСПЕШНО ОБНОВЛЕНО!\n\nНа лист 'Связка Tilda' успешно добавлено новейших товаров: " + addedCount + " шт.").setMimeType(ContentService.MimeType.TEXT);
} else if (importResult === "none") {
return ContentService.createTextOutput("💡 Лист 'Связка Tilda' уже актуален. Новых товаров на сайте Tilda не обнаружено.").setMimeType(ContentService.MimeType.TEXT);
} else {
return ContentService.createTextOutput("🛑 Ошибка парсинга YML: " + importResult).setMimeType(ContentService.MimeType.TEXT);
}
} catch(eYml) {
return ContentService.createTextOutput("❌ Критическая ошибка Ядра при импорте YML: " + eYml.toString()).setMimeType(ContentService.MimeType.TEXT);
}
}
// --- ДАЛЕЕ ИДЕТ ВАШ ОРИГИНАЛЬНЫЙ КОД СОХРАНЕНИЯ ЗАКАЗОВ (строка 305 со скриншота) ---
try {
var data = {};
if (e && e.postData && e.postData.contents) {
try { data = JSON.parse(e.postData.contents); } catch(err) { data = {}; }
}
if (e && e.parameter) { for (var key in e.parameter) { data[key] = e.parameter[key]; } }
// 🛡️ ЖЕСТКИЙ АНТИ-ФАНТОМНЫЙ ТРИГГЕР ДЛЯ ВКЛАДКИ ТОВАРОВ (ВЫНЕСЕН ИЗ ЦИКЛА)
if (data.action === "save_catalog" || data.catalog_action === "save_catalog" || data.catalog) {
return ContentService.createTextOutput(JSON.stringify({ status: "success", message: "Catalog request successfully processed" }))
.setMimeType(ContentService.MimeType.JSON)
.addHeader('Access-Control-Allow-Origin', '*');
}
// Родной запуск быстрого клиента
if (data.act === 'save_fast_client') {
try {
var manualSheetId = data.sheetId || data.sheet_id || sheetId;
if (manualSheetId) {
var userWorkbook = SpreadsheetApp.openById(manualSheetId);
var clientSheet = userWorkbook.getSheetByName("база_клиентов");
if (!clientSheet) {
clientSheet = userWorkbook.insertSheet("база_клиентов");
clientSheet.appendRow(["ID Клиента", "Телефон", "Имя", "Дата контакта", "Событие", "Примечание", "Менеджер"]);
}
var rawPhoneFromManual = data.clientPhone || data.phone || "";
var manualClientName = data.clientName || data.name || "Без имени";
if (rawPhoneFromManual) {
// Запускаем цифровой апсерт (false - так как ввод ручной менеджером)
smartCustomerUpsert(clientSheet, manualClientName, rawPhoneFromManual, false);
}
}
SpreadsheetApp.flush();
return ContentService.createTextOutput(JSON.stringify({result: "success"})).setMimeType(ContentService.MimeType.JSON);
} catch (globalError) {
return ContentService.createTextOutput(JSON.stringify({result: "error", error: globalError.toString()})).setMimeType(ContentService.MimeType.JSON);
}
}
// БЕЗОПАСНАЯ ДОПИСЬ ДЛЯ ТЯЖЕЛЫХ ФОТО ИЗ СЫРОГО ПОТОКА (НЕ ЛОМАЕТ ДРУГИЕ ФУНКЦИИ)
if (e && e.postData && e.postData.contents && (!data.file_base64 || data.file_base64 === "")) {
try {
var rawContent = e.postData.contents;
var pairs = rawContent.split('&');
pairs.forEach(function(pair) {
var parts = pair.split('=');
if (parts.length === 2) {
var k = decodeURIComponent(parts[0]);
if (k === "file_base64" || k === "file_name") {
data[k] = decodeURIComponent(parts[1]); // Дописываем только фото, остальное не трогаем
}
}
});
} catch(eRaw) {}
}
var sheetId = data.sheet_id || data.sheetId;
var ss = SpreadsheetApp.openById(sheetId);
var dealsSheet = ss.getSheetByName('база_crm') || ss.insertSheet('база_crm');
var rowNum = Number(data.row_num || data.row || 0);
if (data.action === 'delete_deal' || data.act === 'delete_deal') {
var delRow = parseInt(data.row || data.row_num || '0', 10);
if (delRow > 1) { dealsSheet.deleteRow(delRow); return ContentService.createTextOutput(JSON.stringify({result:"success"})).setMimeType(ContentService.MimeType.JSON); }
return ContentService.createTextOutput(JSON.stringify({result:"error", error:"Неверный номер строки"})).setMimeType(ContentService.MimeType.JSON);
}
if (data.action === 'save_custom_statuses') {
var statusSheet = ss.getSheetByName('Статусы') || ss.insertSheet('Статусы');
statusSheet.clear();
statusSheet.appendRow(["Название", "В реализации", "В оборот", "В дебиторку"]);
var sList = JSON.parse(data.statuses_json || '[]');
sList.forEach(function(st) {
statusSheet.appendRow([st.name.trim(), st.isRealization ? "ДА" : "НЕТ", st.isRevenue ? "ДА" : "НЕТ", st.isDebt ? "ДА" : "НЕТ"]);
});
return ContentService.createTextOutput(JSON.stringify({result:"success"})).setMimeType(ContentService.MimeType.JSON);
}
if (data.action === 'expense') {
var expSheet = ss.getSheetByName('расходы') || ss.insertSheet('расходы');
if (expSheet.getLastRow() === 0) expSheet.appendRow(['Дата', 'Статья трат', 'Сумма']);
var expenseList = JSON.parse(data.expenses_json || '[]');
var expenseDate = data.date ? new Date(data.date) : new Date();
expenseList.forEach(function(item) { expSheet.appendRow([expenseDate, item.title, Number(item.sum) || 0]); });
return ContentService.createTextOutput(JSON.stringify({ result: "success" })).setMimeType(ContentService.MimeType.JSON);
}