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);
  }


Made on
Tilda