Необязательно — если Apps Script настроен, база казино грузится через него автоматически
✓ При открытии трекера данные автоматически загружаются из таблицы
✓ При добавлении кампании она сразу сохраняется в таблицу
✓ Работает с любого компьютера/браузера
1Google Таблица → Расширения → Apps Script
2Вставь код, разверни: Веб-приложение, Доступ: Все
const SHEET_CAMPS = 'Запуски';
const SHEET_ACCS = 'Аккаунты';
const HEADERS = [
'Нейминг кампании','Профиль','Айди Кабинета','Нейминг адсета','Название крео',
'Страна','Казино','ID Казино','Ссылка ПВА','Отображающаяся ссылка',
'Текст','Заголовок','Описание','Параметры','Пометки',
'Проверка кабов','Вкл/Выкл','Цена лида','Связка',
'CBO/ABO','Топ оффер','Итог','Сервис пва',
'Формат крео','Бюджет $','Стратегия','Bid Amount','Аудитория','Тип крео','Поток','Сетап','Дата',
'Возраст от','Возраст до','Тип кампании','Бонус','Дата акции','Шаблон №'
];
function getSheet(name) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
return ss.getSheetByName(name) || ss.insertSheet(name);
}
function ensureHeaders(sh) {
if (sh.getLastRow() === 0) {
sh.appendRow(HEADERS);
} else {
// Всегда записываем заголовки целиком в первую строку
// Расширяем диапазон если нужно
var currentCols = sh.getLastColumn();
var neededCols = HEADERS.length;
sh.getRange(1, 1, 1, neededCols).setValues([HEADERS]);
}
// Форматируем заголовки
sh.getRange(1, 1, 1, HEADERS.length)
.setFontWeight('bold')
.setBackground('#e8eaf6')
.setFontColor('#1a1a2e');
sh.setFrozenRows(1);
}
function doGet(e) {
const action = e && e.parameter && e.parameter.action;
if (action === 'ping') return json({ok:true, msg:'pong'});
const action = e && e.parameter && e.parameter.action;
// Обновить заголовки во всех листах (добавить новые колонки)
if (action === 'get_campaign_names') {
const sh = getSheet(SHEET_CAMPS);
const last = sh.getLastRow();
if (last < 2) return json({ok:true, names:[]});
const names = sh.getRange(2,1,last-1,1).getValues().flat().map(String).filter(Boolean);
return json({ok:true, names});
}
if (action === 'update_headers') {
const sh = getSheet(SHEET_CAMPS);
ensureHeaders(sh);
return json({ok:true, msg:'Headers updated to '+HEADERS.length+' columns'});
}
if (action === 'read') {
const sh = getSheet(SHEET_CAMPS);
const last = sh.getLastRow();
if (last < 2) return json({ok:true, rows:[]});
const lastCol = Math.max(sh.getLastColumn(), HEADERS.length);
return json({ok:true, rows: sh.getRange(2,1,last-1,lastCol).getValues()});
}
if (action === 'read_buyer_tag') {
const sh = getSheet('Тег Баера');
if (!sh) return json({ok:true, tag:''});
const tag = sh.getRange(1,1).getValue().toString().trim();
return json({ok:true, tag});
}
if (action === 'read_accounts') {
const sh = getSheet(SHEET_ACCS);
const last = sh.getLastRow();
if (last < 2) return json({ok:true, rows:[]});
return json({ok:true, rows: sh.getRange(2,1,last-1,7).getValues()});
}
if (action === 'read_templates') {
const sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Шаблоны');
if (!sh) return json({ok:true, rows:[]});
const last = sh.getLastRow();
if (last < 2) return json({ok:true, rows:[]});
const lastCol = sh.getLastColumn();
return json({ok:true, rows: sh.getRange(2,1,last-1,lastCol).getValues(), headers: sh.getRange(1,1,1,lastCol).getValues()[0]});
}
if (action === 'read_casino_db') {
const sh = getSheet('Id Казино');
if (!sh) return json({ok:true, rows:[]});
const last = sh.getLastRow();
if (last < 2) return json({ok:true, rows:[]});
const lastCol = sh.getLastColumn();
return json({ok:true, rows: sh.getRange(2,1,last-1,lastCol).getValues(), headers: sh.getRange(1,1,1,lastCol).getValues()[0]});
}
return json({ok:true});
}
function doPost(e) {
const d = JSON.parse(e.postData.contents);
const sh = getSheet(SHEET_CAMPS);
ensureHeaders(sh);
if (d.action === 'append' || d.action === 'append_new') {
const name = d.row && d.row[0];
if (!name) return json({ok:false, error:'no name'});
const last = sh.getLastRow();
if (last > 1) {
const names = sh.getRange(2,1,last-1,1).getValues().flat();
const idx = names.indexOf(name);
if (idx >= 0) {
if (d.action === 'append_new') return json({ok:true, action:'skipped'});
sh.getRange(idx+2, 1, 1, d.row.length).setValues([d.row]);
return json({ok:true, action:'updated'});
}
}
sh.appendRow(d.row);
return json({ok:true, action:'added'});
}
if (d.action === 'sync_all' && d.rows) {
sh.clearContents(); sh.clearFormats();
ensureHeaders(sh);
d.rows.forEach(r => sh.appendRow(r));
return json({ok:true});
}
if (d.action === 'clear_formats') {
const last = sh.getLastRow();
if (last > 1) sh.getRange(2,1,last-1,HEADERS.length).setBackground(null);
return json({ok:true});
}
// Запись/обновление аккаунта в лист Аккаунты
if (d.action === 'write_account' && d.row) {
const sh2 = getSheet(SHEET_ACCS);
const cab = d.row[2] ? String(d.row[2]).replace(/\.0$/,'').trim() : '';
if (!cab) return json({ok:false, error:'no cab'});
const last2 = sh2.getLastRow();
// Ищем существующую строку по ID кабинета (колонка C=3)
if (last2 > 1) {
const allRows = sh2.getRange(2,1,last2-1,3).getValues();
let foundIdx = -1;
allRows.forEach(function(row, i) {
// Сравниваем как строки — Sheets может хранить и как число и как текст
const cabVal = String(row[2]||'').replace(/\.0$/,'').trim();
if (cabVal === cab) foundIdx = i;
});
if (foundIdx >= 0) {
const rowNum = foundIdx + 2;
sh2.getRange(rowNum, 1).setValue(d.row[0] || ''); // Кинг
sh2.getRange(rowNum, 2).setValue(d.row[1] || ''); // Активный кинг
// Колонка C (Айди) — обновляем как текст чтобы не было научной нотации
sh2.getRange(rowNum, 3).setNumberFormat('@').setValue(cab);
sh2.getRange(rowNum, 4).setValue(d.row[3] || ''); // Статус
// Колонка E (Трепей) не трогаем
sh2.getRange(rowNum, 6).setValue(d.row[5] || ''); // ГЕО
if (d.row[6] !== undefined) sh2.getRange(rowNum, 7).setValue(d.row[6] || ''); // Пиксели
return json({ok:true, action:'updated'});
}
}
// Новая строка — cab пишем как текст через setNumberFormat
const newRow = last2 + 1;
const rowData = [d.row[0]||'', d.row[1]||'', cab, d.row[3]||'', d.row[4]||'', d.row[5]||'', d.row[6]||''];
sh2.appendRow(rowData);
// Принудительно форматируем колонку C как текст — иначе длинные числа → научная нотация
sh2.getRange(newRow + 1, 3).setNumberFormat('@').setValue(cab);
return json({ok:true, action:'added'});
}
// Обновить спенд и метрики кампаний (из расширения FB CSV)
if (d.action === 'update_spend' && d.campaigns) {
const last = sh.getLastRow();
if (last < 2) return json({ok:true, updated:0});
const names = sh.getRange(2,1,last-1,1).getValues().flat().map(String);
const SPEND_COL=39, CPC_COL=40, CPL_COL=41, CPR_COL=42, CPD_COL=43;
let updated=0;
d.campaigns.forEach(function(camp) {
const idx = names.findIndex(function(n){ return n===camp.name; });
if (idx < 0) return;
const rowNum = idx + 2;
if (camp.spend!==undefined) sh.getRange(rowNum, SPEND_COL).setValue(camp.spend);
if (camp.cpc!==undefined) sh.getRange(rowNum, CPC_COL).setValue(camp.cpc);
if (camp.cpl!==undefined) sh.getRange(rowNum, CPL_COL).setValue(camp.cpl);
if (camp.cpr!==undefined) sh.getRange(rowNum, CPR_COL).setValue(camp.cpr);
if (camp.cpd!==undefined) sh.getRange(rowNum, CPD_COL).setValue(camp.cpd);
updated++;
});
return json({ok:true, updated});
}
return json({ok:true});
}
function json(obj) {
return ContentService.createTextOutput(JSON.stringify(obj))
.setMimeType(ContentService.MimeType.JSON);
}
3Вставь URL выше
Данные
СТАРТОВЫЙ НОМЕР ЗАПУСКА
Текущий: — · Авто = макс. из кампаний + 1
Статистика
Кампаний0
Кабинетов0
Гео0
Активных0
Добавить кабинет
Кинг *
Выбери кинга *
ГЕО
Статус
ID кабинетов * каждый с новой строки
Сменить Кинг
Новый кинг *
Добавить пиксель
ID пикселя *
Описание необяз.
Добавить подход
Название *
В sub1: название-формат
🎬 Video IDs для крео
Укажи Video ID для каждого видео крео. Если ID указан — файл загружать не нужно, FB возьмёт видео из библиотеки.