Skip to main content

GetCourse: сбор статистики в Google Таблице

Скрипт для Google Sheets, который собирает статистику рассылок и количество пользователей в сегментах из GetCourse.

Возможности

  • Письма — выгрузка статистики рассылок (всего, доставлено, просмотры, клики, отписки, ошибки, запрещено)
  • Сегменты — подсчёт пользователей в сегменте по ссылке
  • Мульти-аккаунт — поддержка нескольких аккаунтов GetCourse

Установка

  1. Открыть Google Таблицу
  2. РасширенияApps Script
  3. Вставить код из раздела Код скрипта
  4. Сохранить (Ctrl+S)
  5. Вернуться в таблицу, обновить страницу
  6. В меню появится вкладка GetCourse

Первый запуск

  1. GetCourse → Управление аккаунтами → добавить аккаунт (имя, email, пароль, домен вида https://your-school.getcourse.ru)
  2. GetCourse → Проверить подключение — убедиться, что авторизация работает
  3. При первом запуске Google попросит разрешения — нажать "Разрешить"

Использование

Статистика рассылок

  1. В ячейки вписать ID рассылок (число) или ссылки на рассылки
  2. Выделить эти ячейки
  3. GetCourse → Письма: обновить выбранные
  4. В ячейках справа от ID появятся данные (название, всего, доставлено и т.д.), каждая ячейка с примечанием

Количество пользователей в сегменте

  1. В ячейку вставить ссылку на сегмент (или гиперссылку)
  2. Выделить ячейку
  3. GetCourse → Пользователи: кол-во в сегментах
  4. В ячейке справа — количество, через одну — дата обновления

Код скрипта

/**
 * GC: Email Stats
 *
 * Сбор статистики рассылок GetCourse прямо в Google Таблице.
 *
 * Установка:
 * 1. Расширения → Apps Script → вставить этот код
 * 2. Вписать ID или ссылки рассылок в ячейки
 * 3. Выделить ячейки → меню GC: Email Stats → Обновить выбранные
 */

// ============================================================
// Меню
// ============================================================

function onOpen() {
  var menuName = 'GetCourse';
  var accounts = getAccounts();
  if (accounts.length > 0) {
    var idx = getActiveAccountIndex();
    var acc = accounts[idx];
    menuName = 'GetCourse [' + (acc.name || acc.email) + ']';
  }

  SpreadsheetApp.getUi()
    .createMenu(menuName)
    .addItem('Письма: обновить выбранные', 'updateSelectedRows')
    .addItem('Пользователи: кол-во в сегментах', 'updateSegmentCounts')
    .addSeparator()
    .addItem('Управление аккаунтами', 'showAccountManager')
    .addItem('Проверить подключение', 'testConnection')
    .addToUi();
}

// ============================================================
// Управление аккаунтами
// ============================================================

function getAccounts() {
  var props = PropertiesService.getScriptProperties();
  var json = props.getProperty('gc_accounts');
  if (!json) return [];
  try { return JSON.parse(json); } catch (e) { return []; }
}

function getActiveAccountIndex() {
  var props = PropertiesService.getScriptProperties();
  var idx = parseInt(props.getProperty('gc_active_account') || '0', 10);
  var accounts = getAccounts();
  if (idx >= accounts.length) idx = 0;
  return idx;
}

function saveAccount(accountData) {
  var accounts = getAccounts();
  var props = PropertiesService.getScriptProperties();

  if (accountData.index !== undefined && accountData.index !== null && accountData.index >= 0 && accountData.index < accounts.length) {
    accounts[accountData.index] = {
      name: accountData.name,
      email: accountData.email,
      password: accountData.password,
      domain: accountData.domain
    };
  } else {
    accounts.push({
      name: accountData.name,
      email: accountData.email,
      password: accountData.password,
      domain: accountData.domain
    });
  }

  props.setProperty('gc_accounts', JSON.stringify(accounts));

  if (accounts.length === 1) {
    props.setProperty('gc_active_account', '0');
  }

  return accounts.length - 1;
}

function deleteAccount(index) {
  var accounts = getAccounts();
  var props = PropertiesService.getScriptProperties();

  if (index < 0 || index >= accounts.length) return;
  accounts.splice(index, 1);
  props.setProperty('gc_accounts', JSON.stringify(accounts));

  var activeIdx = getActiveAccountIndex();
  if (index === activeIdx) {
    props.setProperty('gc_active_account', '0');
    CacheService.getScriptCache().remove('gc_session');
  } else if (index < activeIdx) {
    props.setProperty('gc_active_account', String(activeIdx - 1));
  }
}

function setActiveAccount(index) {
  var accounts = getAccounts();
  if (index < 0 || index >= accounts.length) return;
  PropertiesService.getScriptProperties().setProperty('gc_active_account', String(index));
  CacheService.getScriptCache().remove('gc_session');
}

function getAccountsForDialog() {
  var accounts = getAccounts();
  var activeIdx = getActiveAccountIndex();
  return { accounts: accounts, activeIndex: activeIdx };
}

function getConfig() {
  var accounts = getAccounts();
  if (accounts.length === 0) {
    throw new Error('NO_ACCOUNTS: Нет сохранённых аккаунтов. Добавьте аккаунт через меню GetCourse → Управление аккаунтами.');
  }
  var idx = getActiveAccountIndex();
  var acc = accounts[idx];
  return {
    email: acc.email,
    password: acc.password,
    domain: acc.domain
  };
}

// ============================================================
// Диалог управления аккаунтами
// ============================================================

function showAccountManager() {
  var html = HtmlService.createHtmlOutput(getAccountManagerHtml_())
    .setWidth(520)
    .setHeight(480)
    .setTitle('Управление аккаунтами');
  SpreadsheetApp.getUi().showModalDialog(html, 'Управление аккаунтами GetCourse');
}

function getAccountManagerHtml_() {
  return '\
<!DOCTYPE html>\
<html>\
<head>\
<style>\
  * { box-sizing: border-box; font-family: "Google Sans", Roboto, Arial, sans-serif; }\
  body { margin: 0; padding: 16px; font-size: 14px; color: #202124; }\
  h3 { margin: 0 0 12px; font-size: 16px; }\
  .account-list { margin-bottom: 16px; }\
  .account-item {\
    display: flex; align-items: center; padding: 10px 12px;\
    border: 1px solid #dadce0; border-radius: 8px; margin-bottom: 8px;\
    cursor: pointer; transition: background 0.15s;\
  }\
  .account-item:hover { background: #f1f3f4; }\
  .account-item.active { border-color: #1a73e8; background: #e8f0fe; }\
  .account-item .info { flex: 1; margin-left: 10px; }\
  .account-item .name { font-weight: 500; }\
  .account-item .email { font-size: 12px; color: #5f6368; }\
  .account-item .domain { font-size: 11px; color: #80868b; }\
  .account-item .actions { display: flex; gap: 4px; }\
  .btn { padding: 6px 16px; border-radius: 4px; border: 1px solid #dadce0;\
    background: #fff; cursor: pointer; font-size: 13px; }\
  .btn:hover { background: #f1f3f4; }\
  .btn-primary { background: #1a73e8; color: #fff; border: none; }\
  .btn-primary:hover { background: #1557b0; }\
  .btn-danger { color: #d93025; border-color: #d93025; }\
  .btn-danger:hover { background: #fce8e6; }\
  .btn-sm { padding: 4px 10px; font-size: 12px; }\
  .form-group { margin-bottom: 12px; }\
  .form-group label { display: block; margin-bottom: 4px; font-size: 12px; color: #5f6368; }\
  .form-group input { width: 100%; padding: 8px 10px; border: 1px solid #dadce0;\
    border-radius: 4px; font-size: 14px; }\
  .form-group input:focus { outline: none; border-color: #1a73e8; }\
  .form-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }\
  .radio { width: 16px; height: 16px; accent-color: #1a73e8; cursor: pointer; }\
  .empty { text-align: center; padding: 24px; color: #80868b; }\
  #form-section { display: none; }\
</style>\
</head>\
<body>\
\
<div id="list-section">\
  <h3>Аккаунты</h3>\
  <div id="account-list" class="account-list"><div class="empty">Загрузка...</div></div>\
  <button class="btn btn-primary" onclick="showForm(-1)">+ Добавить аккаунт</button>\
</div>\
\
<div id="form-section">\
  <h3 id="form-title">Добавить аккаунт</h3>\
  <input type="hidden" id="edit-index" value="-1">\
  <div class="form-group">\
    <label>Название (для удобства)</label>\
    <input type="text" id="f-name" placeholder="Например: Фитнес Мама">\
  </div>\
  <div class="form-group">\
    <label>Email</label>\
    <input type="email" id="f-email" placeholder="user@example.com">\
  </div>\
  <div class="form-group">\
    <label>Пароль</label>\
    <input type="password" id="f-password" placeholder="Пароль от GetCourse">\
  </div>\
  <div class="form-group">\
    <label>Домен школы</label>\
    <input type="url" id="f-domain" placeholder="https://myschool.getcourse.ru">\
  </div>\
  <div class="form-actions">\
    <button class="btn" onclick="showList()">Отмена</button>\
    <button class="btn btn-primary" onclick="saveForm()">Сохранить</button>\
  </div>\
</div>\
\
<script>\
var currentData = { accounts: [], activeIndex: 0 };\
\
function load() {\
  google.script.run.withSuccessHandler(function(data) {\
    currentData = data;\
    render();\
  }).getAccountsForDialog();\
}\
\
function render() {\
  var list = document.getElementById("account-list");\
  if (currentData.accounts.length === 0) {\
    list.innerHTML = "<div class=\\"empty\\">Нет аккаунтов. Добавьте первый аккаунт.</div>";\
    return;\
  }\
  var html = "";\
  for (var i = 0; i < currentData.accounts.length; i++) {\
    var a = currentData.accounts[i];\
    var isActive = (i === currentData.activeIndex);\
    html += "<div class=\\"account-item" + (isActive ? " active" : "") + "\\">";\
    html += "<input type=\\"radio\\" class=\\"radio\\" name=\\"active\\" " + (isActive ? "checked" : "") + " onclick=\\"activate(" + i + ")\\">";\
    html += "<div class=\\"info\\">";\
    html += "<div class=\\"name\\">" + esc(a.name || "Без названия") + (isActive ? " ✓" : "") + "</div>";\
    html += "<div class=\\"email\\">" + esc(a.email) + "</div>";\
    html += "<div class=\\"domain\\">" + esc(a.domain) + "</div>";\
    html += "</div>";\
    html += "<div class=\\"actions\\">";\
    html += "<button class=\\"btn btn-sm\\" onclick=\\"event.stopPropagation();showForm(" + i + ")\\">Изм.</button>";\
    html += "<button class=\\"btn btn-sm btn-danger\\" onclick=\\"event.stopPropagation();del(" + i + ")\\">✕</button>";\
    html += "</div></div>";\
  }\
  list.innerHTML = html;\
}\
\
function esc(s) { var d = document.createElement("div"); d.textContent = s; return d.innerHTML; }\
\
function activate(idx) {\
  google.script.run.withSuccessHandler(function() {\
    currentData.activeIndex = idx;\
    render();\
  }).setActiveAccount(idx);\
}\
\
function del(idx) {\
  if (!confirm("Удалить аккаунт \\"" + (currentData.accounts[idx].name || currentData.accounts[idx].email) + "\\"?")) return;\
  google.script.run.withSuccessHandler(function() { load(); }).deleteAccount(idx);\
}\
\
function showForm(idx) {\
  document.getElementById("list-section").style.display = "none";\
  document.getElementById("form-section").style.display = "block";\
  document.getElementById("edit-index").value = idx;\
  if (idx >= 0 && idx < currentData.accounts.length) {\
    var a = currentData.accounts[idx];\
    document.getElementById("form-title").textContent = "Редактировать аккаунт";\
    document.getElementById("f-name").value = a.name || "";\
    document.getElementById("f-email").value = a.email || "";\
    document.getElementById("f-password").value = a.password || "";\
    document.getElementById("f-domain").value = a.domain || "";\
  } else {\
    document.getElementById("form-title").textContent = "Добавить аккаунт";\
    document.getElementById("f-name").value = "";\
    document.getElementById("f-email").value = "";\
    document.getElementById("f-password").value = "";\
    document.getElementById("f-domain").value = "";\
  }\
}\
\
function showList() {\
  document.getElementById("form-section").style.display = "none";\
  document.getElementById("list-section").style.display = "block";\
}\
\
function saveForm() {\
  var idx = parseInt(document.getElementById("edit-index").value, 10);\
  var data = {\
    index: idx >= 0 ? idx : null,\
    name: document.getElementById("f-name").value.trim(),\
    email: document.getElementById("f-email").value.trim(),\
    password: document.getElementById("f-password").value,\
    domain: document.getElementById("f-domain").value.trim().replace(/\\/+$/, "")\
  };\
  if (!data.email || !data.password || !data.domain) {\
    alert("Заполните Email, Пароль и Домен.");\
    return;\
  }\
  google.script.run.withSuccessHandler(function() {\
    showList();\
    load();\
  }).saveAccount(data);\
}\
\
load();\
</script>\
</body>\
</html>';
}

// ============================================================
// Авторизация
// ============================================================

/**
 * Логин в GetCourse по email/паролю через AJAX API.
 * GetCourse использует React-форму и эндпоинт /user/public/user/json
 * Возвращает строку cookies для авторизованной сессии.
 */
function login() {
  var config = getConfig();
  var ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';

  // Шаг 1: GET любой страницы для получения начального cookie сессии
  var getResp = UrlFetchApp.fetch(config.domain + '/cms/system/login', {
    method: 'get',
    headers: { 'User-Agent': ua },
    muteHttpExceptions: true,
    followRedirects: true
  });
  var initialCookies = extractAllCookies_(getResp);

  // Шаг 2: AJAX-логин через /user/public/user/json
  var ajaxUrl = config.domain + '/user/public/user/json';
  var postResp = UrlFetchApp.fetch(ajaxUrl, {
    method: 'post',
    headers: {
      'User-Agent': ua,
      'Content-Type': 'application/x-www-form-urlencoded',
      'X-Requested-With': 'XMLHttpRequest',
      'Referer': config.domain + '/cms/system/login',
      'Cookie': initialCookies
    },
    payload: 'action=login&email=' + encodeURIComponent(config.email) +
             '&password=' + encodeURIComponent(config.password),
    muteHttpExceptions: true,
    followRedirects: false
  });

  var postCode = postResp.getResponseCode();
  var postBody = postResp.getContentText();
  var postCookies = extractAllCookies_(postResp);
  var finalCookies = mergeCookies_(initialCookies, postCookies);

  // Парсим JSON-ответ
  var json;
  try {
    json = JSON.parse(postBody);
  } catch (e) {
    throw new Error('LOGIN_FAILED: Неожиданный ответ сервера (HTTP ' + postCode + '): ' + postBody.substring(0, 200));
  }

  // Проверяем результат
  if (json.error) {
    throw new Error('LOGIN_FAILED: ' + (json.error.message || json.error.text || JSON.stringify(json.error)));
  }

  // Шаг 3: Если в ответе есть redirectUrl — переходим по нему (устанавливает финальные cookies)
  if (json.data && json.data.redirectUrl) {
    var redirectUrl = json.data.redirectUrl;
    if (redirectUrl.indexOf('http') !== 0) {
      redirectUrl = config.domain + redirectUrl;
    }
    var redirResp = UrlFetchApp.fetch(redirectUrl, {
      method: 'get',
      headers: { 'User-Agent': ua, 'Cookie': finalCookies },
      muteHttpExceptions: true,
      followRedirects: true
    });
    var redirCookies = extractAllCookies_(redirResp);
    finalCookies = mergeCookies_(finalCookies, redirCookies);
  }

  // Шаг 4: Проверяем что авторизация работает
  var testResp = UrlFetchApp.fetch(config.domain + '/notifications/control/mailings/active', {
    method: 'get',
    headers: { 'User-Agent': ua, 'Cookie': finalCookies },
    muteHttpExceptions: true,
    followRedirects: true
  });
  var testCookies = extractAllCookies_(testResp);
  finalCookies = mergeCookies_(finalCookies, testCookies);

  if (isLoginPage_(testResp.getContentText())) {
    throw new Error('LOGIN_FAILED: Логин прошёл, но сессия не активна. Ответ JSON: ' + postBody.substring(0, 300));
  }

  // Сохраняем cookies в кеш на 2 часа
  CacheService.getScriptCache().put('gc_session', finalCookies, 7200);
  return finalCookies;
}

/**
 * Извлекает ВСЕ cookies из заголовков Set-Cookie ответа.
 * Возвращает строку "name1=val1; name2=val2"
 */
function extractAllCookies_(response) {
  var allHeaders = response.getAllHeaders();
  var setCookieHeaders = allHeaders['Set-Cookie'];
  if (!setCookieHeaders) return '';

  if (typeof setCookieHeaders === 'string') {
    setCookieHeaders = [setCookieHeaders];
  }

  var cookies = {};
  for (var i = 0; i < setCookieHeaders.length; i++) {
    var pair = setCookieHeaders[i].split(';')[0]; // берём только name=value
    var eqIdx = pair.indexOf('=');
    if (eqIdx > 0) {
      var name = pair.substring(0, eqIdx).trim();
      var value = pair.substring(eqIdx + 1).trim();
      cookies[name] = value;
    }
  }

  var parts = [];
  for (var key in cookies) {
    parts.push(key + '=' + cookies[key]);
  }
  return parts.join('; ');
}

/**
 * Объединяет две строки cookies. Новые перезаписывают старые.
 */
function mergeCookies_(existing, fresh) {
  if (!fresh) return existing;
  if (!existing) return fresh;

  var cookies = {};

  // Парсим существующие
  var parts = existing.split(';');
  for (var i = 0; i < parts.length; i++) {
    var eqIdx = parts[i].indexOf('=');
    if (eqIdx > 0) {
      cookies[parts[i].substring(0, eqIdx).trim()] = parts[i].substring(eqIdx + 1).trim();
    }
  }

  // Перезаписываем новыми
  parts = fresh.split(';');
  for (var j = 0; j < parts.length; j++) {
    var eqIdx2 = parts[j].indexOf('=');
    if (eqIdx2 > 0) {
      cookies[parts[j].substring(0, eqIdx2).trim()] = parts[j].substring(eqIdx2 + 1).trim();
    }
  }

  var result = [];
  for (var key in cookies) {
    result.push(key + '=' + cookies[key]);
  }
  return result.join('; ');
}

/**
 * Возвращает актуальный cookie сессии.
 * Сначала проверяет кеш, если нет — логинится заново.
 */
function getSessionCookie() {
  var cached = CacheService.getScriptCache().get('gc_session');
  if (cached) return cached;
  return login();
}

// ============================================================
// HTTP-запрос к GetCourse
// ============================================================

function fetchMailingPage(mailingId) {
  var config = getConfig();
  var url = config.domain + '/notifications/control/mailings/update/id/' + mailingId + '/part/main';
  var sessionCookie = getSessionCookie();

  var html = fetchWithSession_(url, sessionCookie);

  // Проверяем, не попали ли на страницу логина
  if (isLoginPage_(html)) {
    // Сессия истекла — перелогиниваемся
    CacheService.getScriptCache().remove('gc_session');
    sessionCookie = login();
    html = fetchWithSession_(url, sessionCookie);

    if (isLoginPage_(html)) {
      throw new Error('LOGIN_FAILED: Не удалось авторизоваться');
    }
  }

  return html;
}

function fetchWithSession_(url, sessionCookie) {
  var response = UrlFetchApp.fetch(url, {
    method: 'get',
    headers: {
      'Cookie': sessionCookie,
      'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
      'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
      'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7'
    },
    muteHttpExceptions: true,
    followRedirects: true
  });

  var code = response.getResponseCode();
  if (code !== 200) {
    throw new Error('HTTP_ERROR_' + code);
  }

  return response.getContentText();
}

function isLoginPage_(html) {
  return html.indexOf('action="/cms/system/login"') !== -1 ||
         html.indexOf('id="loginForm"') !== -1 ||
         html.indexOf('LoginForm[email]') !== -1 ||
         html.indexOf('LoginForm%5Bemail%5D') !== -1 ||
         html.indexOf('Pagina de logare') !== -1 ||
         html.indexOf('page-cms_system-login') !== -1 ||
         (html.indexOf('login') !== -1 && html.indexOf('Статистика рассылки') === -1 && html.indexOf('gc-user-logined') === -1);
}

// ============================================================
// Парсинг HTML
// ============================================================

function parseMailingStats(html) {
  var stats = {
    title: '',
    total: 0,
    delivered: 0,
    views: 0,
    clicks: 0,
    unsubscribes: 0,
    errors: 0,
    restricted: 0
  };

  // Название рассылки из <title>
  var titleMatch = html.match(/<title>([^<]+)<\/title>/);
  if (titleMatch) {
    stats.title = titleMatch[1].trim();
  }

  // --- Панель 1: "Статистика рассылки" (values-table) ---

  // Всего сообщений
  var totalMatch = html.match(/Всего сообщений<\/td>\s*<td[^>]*>\s*<a[^>]*>\s*([\d\s]+)\s*<\/a>/);
  if (totalMatch) {
    stats.total = parseInt(totalMatch[1].replace(/\s/g, ''), 10) || 0;
  }

  // доставлено
  var deliveredMatch = html.match(/<td class="subkey">\s*доставлено\s*<\/td>\s*<td[^>]*>\s*<a[^>]*>\s*([\d\s]+)\s*<\/a>/);
  if (deliveredMatch) {
    stats.delivered = parseInt(deliveredMatch[1].replace(/\s/g, ''), 10) || 0;
  }

  // ошибка
  var errorsMatch = html.match(/<td class="subkey">\s*ошибка\s*<\/td>\s*<td[^>]*>\s*<a[^>]*>\s*([\d\s]+)\s*<\/a>/);
  if (errorsMatch) {
    stats.errors = parseInt(errorsMatch[1].replace(/\s/g, ''), 10) || 0;
  }

  // запрещено
  var restrictedMatch = html.match(/<td class="subkey">\s*запрещено\s*<\/td>\s*<td[^>]*>\s*<a[^>]*>\s*([\d\s]+)\s*<\/a>/);
  if (restrictedMatch) {
    stats.restricted = parseInt(restrictedMatch[1].replace(/\s/g, ''), 10) || 0;
  }

  // --- Панель 2: "Статистика пользователей" ---

  // просмотров (открытия)
  var viewsMatch = html.match(/([\d\s]+)\s*просмотр[а-я]*\s*\(/);
  if (viewsMatch) {
    stats.views = parseInt(viewsMatch[1].replace(/\s/g, ''), 10) || 0;
  }

  // кликов
  var clicksMatch = html.match(/([\d\s]+)\s*клик[а-я]*\s*\(/);
  if (clicksMatch) {
    stats.clicks = parseInt(clicksMatch[1].replace(/\s/g, ''), 10) || 0;
  }

  // отписки
  var unsubMatch = html.match(/([\d\s]+)\s*отписк[а-яё]*\s*\(/);
  if (unsubMatch) {
    stats.unsubscribes = parseInt(unsubMatch[1].replace(/\s/g, ''), 10) || 0;
  }

  return stats;
}

// ============================================================
// Извлечение ID из ячейки
// ============================================================

function extractMailingId_(cellValue) {
  if (!cellValue) return null;

  var str = String(cellValue).split(/\s*\|\s*/)[0].trim();

  // Если чистое число
  if (/^\d+$/.test(str)) return str;

  // URL с /id/ЧИСЛО
  var urlMatch = str.match(/\/id\/(\d+)/);
  if (urlMatch) return urlMatch[1];

  // Первое число в строке
  var numMatch = str.match(/(\d{5,})/);
  if (numMatch) return numMatch[1];

  return null;
}

// ============================================================
// Обновление статистики
// ============================================================

function updateSelectedRows() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getActiveSheet();
  var selection = sheet.getActiveRange();

  if (!selection) {
    SpreadsheetApp.getUi().alert('Выделите ячейки с ID рассылок.');
    return;
  }

  var updated = 0;
  var numRows = selection.getNumRows();
  var numCols = selection.getNumColumns();
  var startRow = selection.getRow();
  var startCol = selection.getColumn();

  for (var r = 0; r < numRows; r++) {
    for (var c = 0; c < numCols; c++) {
      var cell = sheet.getRange(startRow + r, startCol + c);
      var cellValue = cell.getValue();
      if (!cellValue) continue;

      var mailingId = extractMailingId_(cellValue);
      if (!mailingId) continue;

      ss.toast('Обработка ID ' + mailingId + '...', 'GetCourse', 3);

      var row = startRow + r;
      var col = startCol + c;

      try {
        var html = fetchMailingPage(mailingId);
        var stats = parseMailingStats(html);
        var now = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'dd.MM.yyyy HH:mm');

        var fields = [
          [stats.title, 'Название'],
          [stats.total, 'Всего'],
          [stats.delivered, 'Доставлено'],
          [stats.views, 'Просмотры'],
          [stats.clicks, 'Клики'],
          [stats.unsubscribes, 'Отписки'],
          [stats.errors, 'Ошибки'],
          [stats.restricted, 'Запрещено'],
          [now, 'Обновлено']
        ];

        for (var i = 0; i < fields.length; i++) {
          sheet.getRange(row, col + 1 + i).setValue(fields[i][0]).setNote(fields[i][1]);
        }
        SpreadsheetApp.flush();
        updated++;
      } catch (e) {
        if (e.message.indexOf('LOGIN_FAILED') !== -1) {
          SpreadsheetApp.getUi().alert('Ошибка авторизации: ' + e.message);
          return;
        }
        sheet.getRange(row, col + 1).setValue('Ошибка: ' + e.message).setNote('Ошибка');
        SpreadsheetApp.flush();
      }

      Utilities.sleep(500);
    }
  }

  ss.toast('Обновлено: ' + updated, 'GC: Email Stats', 5);
}

// ============================================================
// Сегменты: количество пользователей
// ============================================================

function extractSegmentUrl_(cell) {
  // HYPERLINK-формула: =HYPERLINK("url", "text")
  var formula = cell.getFormula();
  if (formula) {
    var m = formula.match(/HYPERLINK\s*\(\s*"([^"]+)"/i);
    if (m) return m[1];
  }

  // Rich-text ссылка
  var richText = cell.getRichTextValue();
  if (richText) {
    var url = richText.getLinkUrl();
    if (url) return url;
  }

  // Обычный текст URL
  var val = String(cell.getValue()).trim();
  if (val.match(/^https?:\/\//)) return val;

  return null;
}

function fetchSegmentPage(url) {
  var sessionCookie = getSessionCookie();
  var html = fetchWithSession_(url, sessionCookie);

  if (isLoginPage_(html)) {
    CacheService.getScriptCache().remove('gc_session');
    sessionCookie = login();
    html = fetchWithSession_(url, sessionCookie);

    if (isLoginPage_(html)) {
      throw new Error('LOGIN_FAILED: Не удалось авторизоваться');
    }
  }

  return html;
}

function parseSegmentCount(html) {
  // <div class="summary">Показаны <b>1-30</b> из <b>1 941</b> записи.</div>
  var m = html.match(/<div class="summary">Показаны\s*<b>[^<]+<\/b>\s*из\s*<b>([\d\s]+)<\/b>/);
  if (m) return parseInt(m[1].replace(/\s/g, ''), 10);
  return null;
}

function updateSegmentCounts() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getActiveSheet();
  var selection = sheet.getActiveRange();

  if (!selection) {
    SpreadsheetApp.getUi().alert('Выделите ячейки со ссылками на сегменты.');
    return;
  }

  var updated = 0;
  var numRows = selection.getNumRows();
  var numCols = selection.getNumColumns();
  var startRow = selection.getRow();
  var startCol = selection.getColumn();

  for (var r = 0; r < numRows; r++) {
    for (var c = 0; c < numCols; c++) {
      var cell = sheet.getRange(startRow + r, startCol + c);
      if (!cell.getValue()) continue;

      var url = extractSegmentUrl_(cell);
      if (!url) continue;

      ss.toast('Обработка сегмента...', 'GC: Сегменты', 3);

      try {
        var html = fetchSegmentPage(url);
        var count = parseSegmentCount(html);

        if (count === null) {
          ss.toast('Не удалось найти количество пользователей', 'GC: Сегменты', 3);
          continue;
        }

        var now = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'dd.MM.yyyy HH:mm');
        var col = startCol + c;

        sheet.getRange(startRow + r, col + 1).setValue(count);
        sheet.getRange(startRow + r, col + 2).setValue(now);
        SpreadsheetApp.flush();
        updated++;
      } catch (e) {
        if (e.message.indexOf('LOGIN_FAILED') !== -1) {
          SpreadsheetApp.getUi().alert('Ошибка авторизации: ' + e.message);
          return;
        }
        sheet.getRange(startRow + r, startCol + c + 1).setValue('Ошибка: ' + e.message);
        SpreadsheetApp.flush();
      }

      Utilities.sleep(500);
    }
  }

  ss.toast('Обновлено: ' + updated, 'GC: Сегменты', 5);
}

// ============================================================
// Тест подключения
// ============================================================

function testConnection() {
  var ui = SpreadsheetApp.getUi();
  var config = getConfig();

  try {
    CacheService.getScriptCache().remove('gc_session');
    var sessionCookie = login();
    ui.alert('Подключение успешно!',
      'Email: ' + config.email + '\n' +
      'Домен: ' + config.domain + '\n' +
      'Cookie: ' + sessionCookie.substring(0, 40) + '...\n\n' +
      'Авторизация работает. Можно обновлять статистику.',
      ui.ButtonSet.OK);
  } catch (e) {
    ui.alert('Ошибка подключения', e.message, ui.ButtonSet.OK);
  }
}

/**
 * Отладка сегмента: показывает что возвращает сервер для URL сегмента.
 * Замените testUrl на нужную ссылку, запустите из Apps Script.
 */
function debugSegment() {
  var testUrl = 'https://fitnessmama.school/pl/user/user/index?uc%5Bsegment_id%5D=0&uc%5Brule_string%5D=%7B%22type%22%3A%22user_createdat%22%2C%22inverted%22%3A0%2C%22params%22%3A%7B%22value%22%3A%7B%22from%22%3Anull%2C%22to%22%3Anull%2C%22toNDays%22%3A%2230%22%2C%22fromNDays%22%3Anull%2C%22dateType%22%3A%22last_n_days%22%2C%22withTime%22%3Afalse%7D%2C%22valueMode%22%3Anull%7D%2C%22maxSize%22%3A%22%22%7D&formParams%5Bclarity_uid%5D=JoSTNr9IVThIbcbL_OLoVFNrnDiOXnbX';

  try {
    var html = fetchSegmentPage(testUrl);
    var title = html.match(/<title>([^<]+)<\/title>/);
    var isLogin = isLoginPage_(html);
    var hasFilterCount = html.indexOf('filter-count') !== -1;
    var hasDataCount = html.indexOf('data-count') !== -1;
    var count = parseSegmentCount(html);

    Logger.log('=== DEBUG segment ===');
    Logger.log('URL: ' + testUrl.substring(0, 80) + '...');
    Logger.log('Title: ' + (title ? title[1] : 'не найден'));
    Logger.log('isLoginPage: ' + isLogin);
    Logger.log('hasFilterCount: ' + hasFilterCount);
    Logger.log('hasDataCount: ' + hasDataCount);
    Logger.log('parsedCount: ' + count);
    Logger.log('HTML length: ' + html.length);
    Logger.log('First 2000 chars: ' + html.substring(0, 2000));
  } catch (e) {
    Logger.log('ERROR: ' + e.message);
  }
}

/**
 * Отладка: показывает что возвращает сервер для конкретного mailing ID.
 * Запустите вручную из Apps Script, изменив ID ниже.
 */
function debugMailing() {
  var testId = 4508232; // Замените на нужный ID
  CacheService.getScriptCache().remove('gc_session');

  try {
    var html = fetchMailingPage(testId);
    var title = html.match(/<title>([^<]+)<\/title>/);
    var hasStats = html.indexOf('Статистика рассылки') !== -1;
    var hasUserStats = html.indexOf('просмотр') !== -1;
    var isLogin = isLoginPage_(html);
    var bodyClass = html.match(/class="([^"]*page-[^"]*)"/) || ['', 'не найден'];

    Logger.log('=== DEBUG mailing ID: ' + testId + ' ===');
    Logger.log('Title: ' + (title ? title[1] : 'не найден'));
    Logger.log('Body class: ' + bodyClass[1]);
    Logger.log('isLoginPage: ' + isLogin);
    Logger.log('hasStats: ' + hasStats);
    Logger.log('hasUserStats: ' + hasUserStats);
    Logger.log('HTML length: ' + html.length);
    Logger.log('First 1000 chars: ' + html.substring(0, 1000));
  } catch (e) {
    Logger.log('ERROR: ' + e.message);
  }
}