/**
 * ProLife Home Care — настройка таблицы открытых кейсов
 *
 * Запускается ОДИН раз. Создаёт лист Cases со всеми выпадающими списками,
 * проверками и подсветкой незаполненных полей.
 *
 * Как запустить:
 *   1. Создайте пустую Google Таблицу
 *   2. Расширения → Apps Script
 *   3. Вставьте весь этот код вместо того, что там было
 *   4. Сверху выберите функцию setupJobsSheet и нажмите Run
 *   5. Разрешите доступ, когда Google спросит
 *   6. Вернитесь в таблицу: лист Cases готов
 *
 * Повторный запуск пересоберёт списки, но не тронет уже введённые строки.
 */

// ── Что можно выбирать. Меняйте здесь, потом запустите setupJobsSheet заново ──
var OPTIONS = {
  status:    ['Open', 'Paused', 'Closed'],
  type:      ['HHA', 'PCA', 'HHA or PCA'],
  borough:   ['Brooklyn', 'Queens', 'Manhattan', 'Bronx', 'Staten Island', 'Nassau County'],
  hours:     ['7:00 AM - 3:00 PM', '8:00 AM - 4:00 PM', '9:00 AM - 1:00 PM',
              '9:00 AM - 3:00 PM', '9:00 AM - 5:00 PM', '10:00 AM - 2:00 PM',
              '12:00 PM - 8:00 PM', '3:00 PM - 11:00 PM', '8:00 PM - 8:00 AM',
              '24 hours (live-in)', 'Flexible'],
  days:      ['Mon-Fri', 'Mon-Sun', 'Sat, Sun', 'Mon, Wed, Fri', 'Tue, Thu',
              'Mon, Tue, Wed', 'Thu, Fri, Sat', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
  // От 4 часов с шагом 1. Список длинный, но в Таблицах он с поиском:
  // координатор набирает "3" и сразу видит 30, 31, 32.
  hoursWeek: (function () {
    var out = [];
    for (var h = 4; h <= 84; h++) out.push(String(h));
    return out;
  })(),
  shift:     ['Day', 'Evening', 'Night', 'Live-in', 'Weekends'],
  gender:    ['Female', 'Male', 'Any'],
  genderCl:  ['Female', 'Male'],
  mobility:  ['Ambulatory', 'Walker', 'Wheelchair', 'Bedbound', 'Bedbound + Hoyer'],
  languages: ['English', 'Russian', 'Spanish', 'Ukrainian', 'Chinese (Mandarin & Cantonese)',
              'Georgian', 'Bengali', 'Korean', 'Arabic', 'Haitian Creole', 'Urdu', 'Polish', 'French'],
  // Стандартная ставка одним пунктом. Поле допускает и ручной ввод:
  // для нестандартного кейса координатор просто печатает своё значение.
  rate:      ['$21.57/hour'],
  urgent:    ['No', 'Yes'],

  // Координаторы агентства. Добавился новый: впишите сюда и запустите
  // setupJobsSheet ещё раз, уже введённые строки не пострадают.
  coordinators: ['Irina', 'Anna', 'Tiffany', 'Chris', 'Lucy']
};

// Порядок колонок. Координатор первым: он же и заполняет строку.
var COLUMNS = [
  { title: 'Coordinator',    list: 'coordinators', required: true,  width: 120 },
  { title: 'Status',         list: 'status',       required: true,  width: 90  },
  { title: 'Type',           list: 'type',         required: true,  width: 105 },
  { title: 'ZIP',            zip:  true,           required: true,  width: 70  },
  { title: 'Borough',        list: 'borough',      required: true,  width: 115 },
  { title: 'Hours',          list: 'hours',        required: true,  width: 150 },
  { title: 'Days',           list: 'days',         required: true,  width: 120 },
  { title: 'Hours per week', list: 'hoursWeek',    required: true,  width: 110 },
  { title: 'Shift',          list: 'shift',        required: true,  width: 95  },
  { title: 'Aide gender',    list: 'gender',       required: true,  width: 100 },
  { title: 'Client gender',  list: 'genderCl',     required: true,  width: 105 },
  { title: 'Mobility',       list: 'mobility',     required: true,  width: 140 },
  { title: 'Language 1',     list: 'languages',    required: true,  width: 130 },
  { title: 'Language 2',     list: 'languages',    required: false, width: 130 },
  { title: 'Language 3',     list: 'languages',    required: false, width: 130 },
  { title: 'Rate',           list: 'rate',         required: true,  width: 130, freeInput: true },
  { title: 'Notes',          free: true,           required: false, width: 300 },
  { title: 'Urgent',         list: 'urgent',       required: true,  width: 80  },
  { title: 'Timestamp',      auto: true,           required: false, width: 130 },
  { title: 'Posted',         auto: true,           required: false, width: 130 }
];

var ROWS = 500;      // на сколько строк вперёд ставим списки
var SHEET = 'Cases';

function setupJobsSheet() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();

  /* Лист после импорта CSV называется по имени файла ("cases.csv"),
     поэтому не создаём новый, а переименовываем тот, где уже лежат данные:
     иначе получилось бы два листа и пустой фид на сайте. */
  var sh = ss.getSheetByName(SHEET);
  if (!sh) {
    var sheets = ss.getSheets();
    var withData = null;
    for (var i = 0; i < sheets.length; i++) {
      if (sheets[i].getLastRow() > 0) { withData = sheets[i]; break; }
    }
    sh = withData || sheets[0] || ss.insertSheet();
    sh.setName(SHEET);
  }

  // ── Заголовки ──
  var titles = COLUMNS.map(function (c) { return c.title; });
  sh.getRange(1, 1, 1, titles.length).setValues([titles])
    .setFontWeight('bold').setFontSize(10)
    .setBackground('#1A2E1C').setFontColor('#FFFFFF')
    .setVerticalAlignment('middle').setWrap(true);
  sh.setRowHeight(1, 42);
  sh.setFrozenRows(1);
  sh.setFrozenColumns(1);

  // ── Колонки: ширина, проверки, подсказки ──
  COLUMNS.forEach(function (col, i) {
    var c = i + 1;
    sh.setColumnWidth(c, col.width);
    var body = sh.getRange(2, c, ROWS, 1);
    body.clearDataValidations();

    if (col.list) {
      /* freeInput: список подсказывает стандартное значение, но своё
         тоже принимается. Для ставки это обязательно: кейсы бывают
         нестандартные, а запрет ввода заставил бы координатора
         писать сумму в примечание, откуда сайт её не возьмёт. */
      body.setDataValidation(
        SpreadsheetApp.newDataValidation()
          .requireValueInList(OPTIONS[col.list], true)
          .setAllowInvalid(!!col.freeInput)
          .setHelpText(col.freeInput
            ? 'Выберите из списка или впишите своё значение'
            : 'Выберите значение из списка')
          .build()
      );
    } else if (col.zip) {
      /* В Apps Script нет проверки по регулярке напрямую, поэтому через формулу.
         Ссылка относительная (первая строка диапазона), Google размножит её вниз. */
      var cell = colLetter(c) + '2';
      body.setDataValidation(
        SpreadsheetApp.newDataValidation()
          .requireFormulaSatisfied('=REGEXMATCH(TO_TEXT(' + cell + '), "^[0-9]{5}$")')
          .setAllowInvalid(false)
          .setHelpText('Ровно 5 цифр, например 11235. Без ZIP кейс на сайт не попадёт.')
          .build()
      );
      body.setNumberFormat('@');   // иначе 07030 превратится в 7030
    } else if (col.date) {
      body.setDataValidation(
        SpreadsheetApp.newDataValidation().requireDate()
          .setAllowInvalid(false)
          .setHelpText('Дата, после которой кейс скрывается. Можно не заполнять.')
          .build()
      );
      body.setNumberFormat('yyyy-mm-dd');
    } else if (col.auto) {
      body.setNumberFormat('yyyy-mm-dd hh:mm');
    }
  });

  // ── Подсветка пустых обязательных полей ──
  // Красим только те строки, где координатор уже начал заполнять: пустой
  // лист не должен встречать человека стеной красного.
  var rules = [];
  COLUMNS.forEach(function (col, i) {
    if (!col.required) return;
    var letter = colLetter(i + 1);
    rules.push(
      SpreadsheetApp.newConditionalFormatRule()
        .whenFormulaSatisfied('=AND($' + colLetter(colIndex('Coordinator')) + '2<>"", ' + letter + '2="")')
        .setBackground('#FCE8E6')
        .setRanges([sh.getRange(2, i + 1, ROWS, 1)])
        .build()
    );
  });

  // Закрытые кейсы приглушаем, срочные помечаем
  rules.push(
    SpreadsheetApp.newConditionalFormatRule()
      .whenFormulaSatisfied('=$' + colLetter(colIndex('Status')) + '2="Closed"')
      .setBackground('#F1F3F4').setFontColor('#9AA0A6')
      .setRanges([sh.getRange(2, 1, ROWS, COLUMNS.length)])
      .build()
  );
  rules.push(
    SpreadsheetApp.newConditionalFormatRule()
      .whenFormulaSatisfied('=$' + colLetter(colIndex('Urgent')) + '2="Yes"')
      .setBackground('#FEF7E0')
      .setRanges([sh.getRange(2, colIndex('Urgent'), ROWS, 1)])
      .build()
  );
  sh.setConditionalFormatRules(rules);

  // ── Мелочи для удобства ──
  sh.getRange(2, 1, ROWS, COLUMNS.length).setVerticalAlignment('middle').setFontSize(10);
  sh.getRange(2, colIndex('Notes'), ROWS, 1).setWrap(true);   // Notes переносим по словам
  if (sh.getMaxColumns() > COLUMNS.length) {
    sh.deleteColumns(COLUMNS.length + 1, sh.getMaxColumns() - COLUMNS.length);
  }

  // Публичный лист: наружу уходит он, а не Cases
  buildPublicSheet();

  // Инструкция на отдельном листе, чтобы не мешала работе
  buildHelpSheet(ss);

  // Защита ставится последней: раньше листов, которые надо защитить,
  // ещё не существует
  protectSheets();

  /* Всплывающая подсказка, а не alert: alert показывается во вкладке
     с таблицей и ждёт нажатия OK, а скрипт запускают из редактора,
     где выглядит, будто он завис. */
  ss.toast('Лист Cases настроен. Дальше: Файл → Поделиться → Опубликовать в интернете → CSV.',
           'Готово', 12);
  Logger.log('Готово. Колонок: %s, строк со списками: %s', COLUMNS.length, ROWS);
}

/**
 * Дата ставится сама, когда координатор выбрал себя в первой колонке.
 * Заполнять её руками никто не будет, а без даты порядок кейсов на сайте
 * становится случайным.
 */
/** Номер колонки по заголовку. Считать позиции руками нельзя:
    стоит вставить колонку в середину, и все смещения поедут. */
function colIndex(title) {
  for (var i = 0; i < COLUMNS.length; i++) {
    if (COLUMNS[i].title === title) return i + 1;
  }
  return 0;
}

/**
 * Возвращает на место проверку данных в одной ячейке.
 *
 * Правила берутся из того же описания COLUMNS, что и при первичной
 * настройке: два независимых списка правил однажды разъехались бы.
 */
function restoreValidation(sh, row, col) {
  var def = COLUMNS[col - 1];
  if (!def || row < 2) return;

  var cell = sh.getRange(row, col);

  if (def.list) {
    cell.setDataValidation(
      SpreadsheetApp.newDataValidation()
        .requireValueInList(OPTIONS[def.list], true)
        .setAllowInvalid(!!def.freeInput)
        .setHelpText(def.freeInput
          ? 'Выберите из списка или впишите своё значение'
          : 'Выберите значение из списка')
        .build()
    );
  } else if (def.zip) {
    cell.setDataValidation(
      SpreadsheetApp.newDataValidation()
        .requireFormulaSatisfied('=REGEXMATCH(TO_TEXT(' + colLetter(col) + row + '), "^[0-9]{5}$")')
        .setAllowInvalid(false)
        .setHelpText('Ровно 5 цифр, например 11235. Без ZIP кейс на сайт не попадёт.')
        .build()
    );
  }
}

function onEdit(e) {
  if (!e || !e.range) return;
  var sh = e.range.getSheet();
  if (sh.getName() !== SHEET) return;

  var row = e.range.getRow(), col = e.range.getColumn();
  if (row < 2 || col > COLUMNS.length) return;

  var stampCol = colIndex('Timestamp');
  var zipCol   = colIndex('ZIP');

  /* Вставка через Cmd+V переносит вместе со значением проверку данных
     из скопированной ячейки, то есть стирает нашу. Возвращаем её на место
     и подсвечиваем, если вставили не пять цифр: иначе строка молча
     не попадёт на сайт, а координатор будет думать, что всё в порядке. */
  /* Проверка данных восстанавливается после каждой правки.

     В Google Таблицах нет права "вводить значения, но не менять настройки
     поля": у кого есть доступ на редактирование, тот может снять выпадающий
     список. Чаще это происходит не нарочно: вставка через Cmd+V переносит
     настройки из скопированной ячейки и затирает наши.

     Поэтому не запрещаем, а возвращаем на место: настройка держится
     сама, и снять её насовсем не получится. */
  restoreValidation(sh, row, col);

  if (col === zipCol) {
    var cell = e.range;
    var val  = String(cell.getValue()).trim();
    if (val && !/^\d{5}$/.test(val)) {
      cell.setBackground('#F4C7C3').setNote('ZIP должен быть ровно 5 цифр. Сейчас: "' + val + '". Кейс на сайт не попадёт.');
      SpreadsheetApp.getActiveSpreadsheet().toast('ZIP "' + val + '" не подходит: нужно ровно 5 цифр', 'Проверьте строку ' + row, 8);
    } else {
      cell.setBackground(null).clearNote();
    }
  }

  if (col === stampCol) return;

  var stamp = sh.getRange(row, stampCol);
  if (!stamp.getValue() && sh.getRange(row, 1).getValue()) {
    stamp.setValue(new Date());
  }

  // Новая строка по умолчанию Open: статус забывают чаще всего
  var status = sh.getRange(row, colIndex('Status'));
  if (!status.getValue() && sh.getRange(row, 1).getValue()) {
    status.setValue('Open');
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   Публичный лист

   Наружу публикуется этот лист, а не Cases. В Cases лежат имена
   координаторов, примечания по пациентам и мобильность: ссылка на
   опубликованный CSV открывается без пароля кем угодно, и там этих
   данных быть не должно.

   Лист собирается формулой, то есть обновляется сам вместе с Cases.
   Отдельная копия рано или поздно разъехалась бы с оригиналом.
   ═════════════════════════════════════════════════════════════════════════ */

var PUBLIC_SHEET = 'Public';

// Что уходит на сайт. Всё остальное остаётся внутри агентства.
var PUBLIC_COLUMNS = [
  'Status', 'Type', 'ZIP', 'Borough', 'Hours', 'Days',
  'Hours per week', 'Shift', 'Aide gender',
  'Language 1', 'Language 2', 'Language 3', 'Rate', 'Urgent', 'Timestamp'
];

/**
 * Снимает наши защиты с листа перед тем, как переписать его из скрипта.
 *
 * Жёстко защищённый лист не даст записать в себя даже нашей же функции,
 * если её запустил не владелец. Возвращаем false, чтобы вызывающий код
 * объяснил человеку, что делать, а не упал с системной ошибкой.
 */
function unlockSheet(sh) {
  try {
    sh.getProtections(SpreadsheetApp.ProtectionType.SHEET).forEach(function (p) {
      if (p.getDescription() && p.getDescription().indexOf('ProLife') === 0) p.remove();
    });
    return true;
  } catch (err) {
    return false;
  }
}

function buildPublicSheet() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var src = ss.getSheetByName(SHEET);
  if (!src) { ss.toast('Лист ' + SHEET + ' не найден', 'Ошибка', 8); return; }

  var sh = ss.getSheetByName(PUBLIC_SHEET);
  if (!sh) sh = ss.insertSheet(PUBLIC_SHEET);

  if (!unlockSheet(sh)) {
    ss.toast('Лист Public защищён, пересобрать его может только владелец таблицы.',
             'Недостаточно прав', 10);
    return;
  }
  sh.clear();

  // Буквы колонок в Cases для каждой публичной колонки
  var letters = PUBLIC_COLUMNS.map(function (title) {
    var i = colIndex(title);
    return i ? colLetter(i) : null;
  }).filter(String);

  sh.getRange(1, 1, 1, PUBLIC_COLUMNS.length).setValues([PUBLIC_COLUMNS])
    .setFontWeight('bold').setFontSize(10)
    .setBackground('#1A2E1C').setFontColor('#FFFFFF');
  sh.setFrozenRows(1);

  /* Только строки со статусом Open: закрытые кейсы наружу не отдаём вовсе.
     Раньше они уходили в CSV и просто отфильтровывались на сайте, то есть
     история закрытых кейсов лежала в открытом доступе. */
  var range = letters.map(function (L) { return "'" + SHEET + "'!" + L + '2:' + L; }).join(', ');
  var statusL = colLetter(colIndex('Status'));
  var formula = '=IFERROR(FILTER({' + range + "}, '" + SHEET + "'!" + statusL + '2:' + statusL + '="Open"), "")';

  sh.getRange(2, 1).setFormula(formula);

  // Предупреждение сбоку, чтобы никто не начал вносить сюда правки руками
  sh.getRange(1, PUBLIC_COLUMNS.length + 2).setValue(
    'Этот лист собирается сам из Cases и публикуется в интернет. ' +
    'Не редактируйте его вручную и не добавляйте сюда колонки с личными данными.'
  ).setFontColor('#C0392B').setFontWeight('bold').setWrap(true);
  sh.setColumnWidth(PUBLIC_COLUMNS.length + 2, 340);

  ss.toast('Лист Public создан. Публикуйте в интернет именно его, а не Cases.', 'Готово', 12);
}

/* ═══════════════════════════════════════════════════════════════════════════
   Защита от случайных правок

   Защита предупреждающая, а не жёсткая. Жёсткая блокирует запись всем,
   кроме владельца, а наши функции (дата в Timestamp, отметка Posted,
   архивация, пересборка Public) выполняются от имени того, кто нажал
   кнопку. С жёсткой защитой они молча падали бы у всех, кроме одного
   человека, и это заметили бы не сразу.

   Предупреждение останавливает случайное движение мышью и объясняет,
   что именно сломается. Осознанную правку оно пропустит: от намеренных
   действий сотрудников защищают права доступа к файлу, а не скрипт.
   ═════════════════════════════════════════════════════════════════════════ */

function protectSheets() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var done = [];

  // Снимаем прежние защиты, чтобы при повторном запуске не плодить дубли
  ['RANGE', 'SHEET'].forEach(function (type) {
    ss.getProtections(SpreadsheetApp.ProtectionType[type]).forEach(function (p) {
      if (p.getDescription() && p.getDescription().indexOf('ProLife') === 0) p.remove();
    });
  });

  // Заголовки Cases: по ним код находит колонки. Переименовали - фид пуст.
  var cases = ss.getSheetByName(SHEET);
  if (cases) {
    /* Заголовки жёстко: по ним код находит колонки, и переименование
       одной ячейки оставляет сайт без списка вакансий. Строки с кейсами
       не трогаем, координаторы работают как раньше. */
    var hd = cases.getRange(1, 1, 1, COLUMNS.length).protect()
      .setDescription('ProLife: заголовки');
    hd.removeEditors(hd.getEditors());
    if (hd.canDomainEdit()) hd.setDomainEdit(false);
    done.push('заголовки Cases');
  }

  /* Public закрываем жёстко: там формула, и правка ломает выгрузку
     на сайт целиком. Координатору в этом листе делать нечего.
     Редактировать сможет только тот, кто запустил защиту, обычно владелец. */
  var pub = ss.getSheetByName(PUBLIC_SHEET);
  if (pub) {
    var pp = pub.protect().setDescription('ProLife: публичный лист, собирается сам');
    pp.removeEditors(pp.getEditors());
    if (pp.canDomainEdit()) pp.setDomainEdit(false);
    done.push(PUBLIC_SHEET + ' (только владелец)');
  }

  /* Архив предупреждением, а не жёстко: в него пишет archiveClosed,
     а она выполняется от имени того, кто нажал кнопку меню. */
  var arch = ss.getSheetByName(ARCHIVE);
  if (arch) {
    arch.protect().setDescription('ProLife: ' + ARCHIVE).setWarningOnly(true);
    done.push(ARCHIVE);
  }

  // Инструкцию закрываем жёстко: её только читают
  var help = ss.getSheetByName('Как заполнять');
  if (help) {
    var hp = help.protect().setDescription('ProLife: инструкция');
    hp.removeEditors(hp.getEditors());
    if (hp.canDomainEdit()) hp.setDomainEdit(false);
    done.push('инструкция');
  }

  ss.toast('Защита включена: ' + done.join(', ') + '.', 'Готово', 10);
}

/* ═══════════════════════════════════════════════════════════════════════════
   Архив закрытых кейсов
   ═════════════════════════════════════════════════════════════════════════ */

var ARCHIVE = 'Архив';

/** Меню в таблице. Появляется само при открытии файла. */
function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('ProLife')
    .addItem('Текст объявления для Facebook и Reddit', 'copyPostText')
    .addItem('Пост для Google Business Profile', 'copyGbpPost')
    .addItem('Опубликовать в Telegram сейчас', 'postToTelegram')
    .addSeparator()
    .addItem('Убрать закрытые кейсы в архив', 'archiveClosed')
    .addSeparator()
    .addSubMenu(SpreadsheetApp.getUi().createMenu('Настройки')
      .addItem('Состояние публикации', 'autoPostingStatus')
      .addItem('Подключить Telegram-бота', 'setupTelegram')
      .addItem('Включить автопостинг', 'enableAutoPosting')
      .addItem('Выключить автопостинг', 'disableAutoPosting')
      .addSeparator()
      .addItem('Пересобрать списки и оформление', 'setupJobsSheet')
      .addItem('Пересобрать публичный лист', 'buildPublicSheet')
      .addItem('Включить защиту от правок', 'protectSheets'))
    .addToUi();
}

/**
 * Переносит строки со статусом Closed на лист "Архив".
 *
 * Именно переносит, а не удаляет: закрытый кейс пригодится, когда та же
 * семья вернётся или понадобится вспомнить, кто там работал. Строки на
 * паузе не трогаем, они ещё вернутся в работу.
 */
function archiveClosed() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sh = ss.getSheetByName(SHEET);
  if (!sh) { ss.toast('Лист ' + SHEET + ' не найден', 'Ошибка', 8); return; }

  var lastRow = sh.getLastRow();
  if (lastRow < 2) { ss.toast('Кейсов пока нет', 'Архив', 6); return; }

  var width = COLUMNS.length;
  var data  = sh.getRange(2, 1, lastRow - 1, width).getValues();

  var closed = [];
  var rowsToDelete = [];
  for (var i = 0; i < data.length; i++) {
    if (String(data[i][1]).trim().toLowerCase() === 'closed') {
      closed.push(data[i].concat([new Date()]));   // + дата архивации
      rowsToDelete.push(i + 2);
    }
  }

  if (!closed.length) { ss.toast('Закрытых кейсов нет', 'Архив', 6); return; }

  // Лист архива: создаём при первом запуске
  var arch = ss.getSheetByName(ARCHIVE);
  if (!arch) {
    arch = ss.insertSheet(ARCHIVE);
    var titles = COLUMNS.map(function (c) { return c.title; }).concat(['Archived']);
    arch.getRange(1, 1, 1, titles.length).setValues([titles])
      .setFontWeight('bold').setFontSize(10)
      .setBackground('#5C6B64').setFontColor('#FFFFFF');
    arch.setFrozenRows(1);
  }

  arch.getRange(arch.getLastRow() + 1, 1, closed.length, width + 1).setValues(closed);
  arch.getRange(2, width + 1, arch.getLastRow(), 1).setNumberFormat('yyyy-mm-dd hh:mm');

  // Удаляем снизу вверх: иначе после первого удаления номера строк съедут
  rowsToDelete.sort(function (a, b) { return b - a; });
  rowsToDelete.forEach(function (r) { sh.deleteRow(r); });

  ss.toast('Перенесено в архив: ' + closed.length, 'Готово', 8);
}

/* ═══════════════════════════════════════════════════════════════════════════
   Публикация кейсов
   
   Telegram публикуется сам. Facebook и Reddit только вручную:
   Meta закрыла Groups API в апреле 2024, а на Reddit автопостинг вакансий
   ведёт к бану по правилам сообществ. Для них готовим текст, вставку
   человек делает сам.
   ═════════════════════════════════════════════════════════════════════════ */

/* Номера колонок считаются по заголовкам, а не прописаны числами:
   стоит вставить колонку в середину, и все смещения поедут молча,
   а публикация начнёт брать данные из соседних полей. */
var COL_STATUS  = colIndex('Status'),      COL_TYPE   = colIndex('Type'),
    COL_ZIP     = colIndex('ZIP'),         COL_BOROUGH = colIndex('Borough'),
    COL_HOURS   = colIndex('Hours'),       COL_DAYS   = colIndex('Days'),
    COL_WEEK    = colIndex('Hours per week'), COL_SHIFT = colIndex('Shift'),
    COL_AIDE    = colIndex('Aide gender'), COL_MOB    = colIndex('Mobility'),
    COL_LANG1   = colIndex('Language 1'),  COL_RATE   = colIndex('Rate'),
    COL_NOTES   = colIndex('Notes'),       COL_URGENT = colIndex('Urgent'),
    COL_POSTED  = colIndex('Posted');

var PL_PHONE = '(718) 232-2777';
var PL_PAGE  = 'https://prolifehc.com/hha-pca-jobs/';

/** Токен и чат хранятся в свойствах скрипта, а не в коде: код видят все,
    у кого есть доступ к таблице, а бот с чужим токеном рассылает что угодно. */
function setupTelegram() {
  var ui = SpreadsheetApp.getUi();

  var t = ui.prompt('Telegram: шаг 1 из 2',
    'Вставьте токен бота (получить у @BotFather: /newbot).',
    ui.ButtonSet.OK_CANCEL);
  if (t.getSelectedButton() !== ui.Button.OK || !t.getResponseText().trim()) return;

  var c = ui.prompt('Telegram: шаг 2 из 2',
    'ID канала или группы. Для публичного канала: @имя_канала.\n' +
    'Бот должен быть добавлен в канал администратором с правом публикации.',
    ui.ButtonSet.OK_CANCEL);
  if (c.getSelectedButton() !== ui.Button.OK || !c.getResponseText().trim()) return;

  var props = PropertiesService.getScriptProperties();
  props.setProperty('TG_TOKEN', t.getResponseText().trim());
  props.setProperty('TG_CHAT',  c.getResponseText().trim());

  // Проверяем сразу: молчаливо сохранить неверный токен хуже, чем не сохранить
  var test = tgSend('ProLife: бот подключён. Здесь будут появляться открытые кейсы.');
  ui.alert(test.ok ? 'Готово' : 'Не получилось',
    test.ok ? 'Тестовое сообщение отправлено в канал.'
            : 'Telegram ответил: ' + test.error + '\n\nПроверьте токен и то, что бот админ канала.',
    ui.ButtonSet.OK);
}

function tgSend(text) {
  var props = PropertiesService.getScriptProperties();
  var token = props.getProperty('TG_TOKEN'), chat = props.getProperty('TG_CHAT');
  if (!token || !chat) return { ok: false, error: 'Бот не настроен' };

  try {
    var res = UrlFetchApp.fetch('https://api.telegram.org/bot' + token + '/sendMessage', {
      method: 'post',
      contentType: 'application/json',
      muteHttpExceptions: true,
      payload: JSON.stringify({
        chat_id: chat,
        text: text,
        parse_mode: 'HTML',
        disable_web_page_preview: false
      })
    });
    var body = JSON.parse(res.getContentText());
    return body.ok ? { ok: true } : { ok: false, error: body.description || 'неизвестная ошибка' };
  } catch (e) {
    return { ok: false, error: String(e) };
  }
}

/** Справочные значения на русский: объявление читают русскоязычные сиделки,
    а таблицу координаторы ведут по-английски. Совпадает со словарём сайта. */
var TR = {
  'Brooklyn': 'Бруклин', 'Queens': 'Куинс', 'Manhattan': 'Манхэттен',
  'Bronx': 'Бронкс', 'Staten Island': 'Стейтен-Айленд', 'Nassau County': 'округ Нассау',
  'HHA': 'HHA', 'PCA': 'PCA', 'HHA or PCA': 'HHA или PCA',
  'Day': 'день', 'Evening': 'вечер', 'Night': 'ночь',
  'Live-in': 'с проживанием', 'Weekends': 'выходные',
  'Female': 'женщина', 'Male': 'мужчина', 'Any': 'не важно',
  'Ambulatory': 'ходячий', 'Walker': 'с ходунками', 'Wheelchair': 'в коляске',
  'Bedbound': 'лежачий', 'Bedbound + Hoyer': 'лежачий, нужен подъёмник',
  'English': 'английский', 'Russian': 'русский', 'Spanish': 'испанский',
  'Ukrainian': 'украинский', 'Chinese (Mandarin & Cantonese)': 'китайский',
  'Georgian': 'грузинский', 'Bengali': 'бенгальский', 'Korean': 'корейский',
  'Arabic': 'арабский', 'Haitian Creole': 'гаитянский креольский',
  'Urdu': 'урду', 'Polish': 'польский', 'French': 'французский',
  'Mon-Fri': 'Пн-Пт', 'Mon-Sun': 'Пн-Вс', 'Sat, Sun': 'Сб, Вс',
  'Mon, Wed, Fri': 'Пн, Ср, Пт', 'Tue, Thu': 'Вт, Чт', 'Mon, Tue, Wed': 'Пн, Вт, Ср',
  'Thu, Fri, Sat': 'Чт, Пт, Сб',
  'Mon': 'Пн', 'Tue': 'Вт', 'Wed': 'Ср', 'Thu': 'Чт', 'Fri': 'Пт', 'Sat': 'Сб', 'Sun': 'Вс',
  'Flexible': 'гибкий график', '24 hours (live-in)': 'сутки, с проживанием'
};

function tr(value) {
  var v = String(value || '').trim();
  return TR.hasOwnProperty(v) ? TR[v] : v;
}

/** Собирает текст объявления по строке таблицы. */
function buildPost(row, html) {
  function v(i) { return String(row[i - 1] || '').trim(); }
  var b = html ? function (s) { return '<b>' + s + '</b>'; } : function (s) { return s; };

  var langs = [v(COL_LANG1), v(COL_LANG1 + 1), v(COL_LANG1 + 2)].filter(String);
  var lines = [];

  lines.push((v(COL_URGENT) === 'Yes' ? 'СРОЧНО. ' : '') +
             b('Нужна сиделка ' + (tr(v(COL_TYPE)) || 'HHA/PCA')));
  lines.push('');
  lines.push('Район: ' + tr(v(COL_BOROUGH)) + ', ZIP ' + v(COL_ZIP));
  if (v(COL_HOURS)) lines.push('Часы: ' + tr(v(COL_HOURS)));
  if (v(COL_DAYS))  lines.push('Дни: ' + tr(v(COL_DAYS)));
  if (v(COL_WEEK))  lines.push('В неделю: ' + v(COL_WEEK) + ' часов');
  if (v(COL_SHIFT)) lines.push('Смена: ' + tr(v(COL_SHIFT)));
  if (langs.length) lines.push('Языки: ' + langs.map(tr).join(', '));
  /* Мобильность пациента и примечание в объявление не идут: канал
     публичный, а ZIP плюс состояние здоровья плюс график сужают круг
     до одного человека в квартале. Координатор расскажет по телефону. */
  if (v(COL_AIDE) && v(COL_AIDE) !== 'Any') lines.push('Просьба клиента: сиделка ' + tr(v(COL_AIDE)));
  if (v(COL_RATE))  lines.push('Ставка: ' + v(COL_RATE));

  lines.push('');
  lines.push('Звоните: ' + PL_PHONE);
  lines.push(PL_PAGE);

  return lines.join('\n');
}

/**
 * Публикует в Telegram кейсы, которых там ещё не было.
 * Отметка в колонке Posted: без неё повторный запуск завалил бы канал дублями.
 */
function postToTelegram() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sh = ss.getSheetByName(SHEET);
  if (!sh) { ss.toast('Лист ' + SHEET + ' не найден', 'Ошибка', 8); return; }

  var props = PropertiesService.getScriptProperties();
  if (!props.getProperty('TG_TOKEN')) {
    ss.toast('Сначала: меню ProLife → Настроить Telegram', 'Бот не настроен', 10);
    return;
  }

  var last = sh.getLastRow();
  if (last < 2) { ss.toast('Кейсов нет', 'Telegram', 6); return; }

  var data = sh.getRange(2, 1, last - 1, COLUMNS.length).getValues();
  var sent = 0, failed = 0, firstError = '';

  for (var i = 0; i < data.length; i++) {
    var row = data[i];
    if (String(row[COL_STATUS - 1]).trim().toLowerCase() !== 'open') continue;
    if (row[COL_POSTED - 1]) continue;                       // уже публиковали
    if (!/^\d{5}$/.test(String(row[COL_ZIP - 1]).trim())) continue;  // как и на сайте
    if (!isRowComplete(row)) continue;                       // строка ещё заполняется

    var res = tgSend(buildPost(row, true));
    if (res.ok) {
      sh.getRange(i + 2, COL_POSTED).setValue(new Date());
      sent++;
      Utilities.sleep(1200);   // Telegram ограничивает частоту отправки
    } else {
      failed++;
      if (!firstError) firstError = res.error;
    }
  }

  var msg = sent ? ('Опубликовано: ' + sent) : 'Новых кейсов для публикации нет';
  if (failed) msg += '. Ошибок: ' + failed + ' (' + firstError + ')';
  ss.toast(msg, 'Telegram', 10);
}

/* ── Google Business Profile ───────────────────────────────────────────────
   Один сводный пост в неделю, а не пост на каждый кейс: в карточке по
   умолчанию виден только самый свежий, остальные приходится листать.
   Отдельные посты просто прятали бы друг друга.

   Формат по рекомендациям Google: 80-100 слов, главное в начале,
   призыв к действию. Хештеги там не кликабельны и пользы не дают.
   ───────────────────────────────────────────────────────────────────────── */

function buildGbpPost() {
  var sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET);
  if (!sh || sh.getLastRow() < 2) return null;

  var data = sh.getRange(2, 1, sh.getLastRow() - 1, COLUMNS.length).getValues();
  var open = [];

  for (var i = 0; i < data.length; i++) {
    var r = data[i];
    if (String(r[COL_STATUS - 1]).trim().toLowerCase() !== 'open') continue;
    if (!/^\d{5}$/.test(String(r[COL_ZIP - 1]).trim())) continue;
    open.push(r);
  }
  if (!open.length) return null;

  // Срочные первыми: у поста ограничение по длине, и обрезаться должны не они
  open.sort(function (a, b) {
    var ua = String(a[COL_URGENT - 1]).trim() === 'Yes' ? 0 : 1;
    var ub = String(b[COL_URGENT - 1]).trim() === 'Yes' ? 0 : 1;
    return ua - ub;
  });

  var lines = [];
  var shown = 0;

  for (var j = 0; j < open.length && shown < 6; j++) {
    var r = open[j];
    function v(c) { return String(r[c - 1] || '').trim(); }

    var area = v(COL_BOROUGH) === 'Bronx' ? 'The Bronx' : v(COL_BOROUGH);
    var head = (area ? area + ' ' : '') + v(COL_ZIP);

    // Только то, что заполнено. Пустые поля не превращаются в "не указано".
    var parts = [];
    if (v(COL_TYPE))  parts.push(v(COL_TYPE));
    if (v(COL_DAYS))  parts.push(v(COL_DAYS));
    if (v(COL_HOURS)) parts.push(v(COL_HOURS));
    if (v(COL_WEEK))  parts.push(v(COL_WEEK) + ' hrs/week');

    var langs = [];
    for (var k = 0; k < 3; k++) {
      var l = v(COL_LANG1 + k).replace(/\s*\(.*?\)/, '');
      if (l) langs.push(l);
    }
    if (langs.length) parts.push(listOut(langs));

    lines.push(head + ': ' + parts.join(', ') + '.');
    shown++;
  }

  var rest = open.length - shown;
  if (rest > 0) lines.push('And ' + rest + ' more open case' + (rest === 1 ? '' : 's') + '.');

  var text = 'HHA and PCA openings, updated ' +
             Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'MMMM d') + '. ' +
             lines.join(' ') + ' Call ' + PL_PHONE + '.';

  return {
    text: text,
    cta:  PL_PAGE + '?utm_source=google&utm_medium=gbp_post&utm_campaign=open_cases',
    total: open.length
  };
}

/** "Brooklyn, Queens and the Bronx" вместо "Brooklyn, Queens, Bronx" */
function listOut(arr) {
  if (arr.length === 1) return arr[0];
  return arr.slice(0, -1).join(', ') + ' and ' + arr[arr.length - 1];
}

function copyGbpPost() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var post = buildGbpPost();

  if (!post) { ss.toast('Открытых кейсов нет, постить нечего', 'Google Business Profile', 8); return; }

  var tz = Session.getScriptTimeZone();
  var today = new Date();
  var end = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000);
  var fmt = function (d) { return Utilities.formatDate(d, tz, 'MM/dd/yyyy'); };

  var words = post.text.split(/\s+/).length;
  var note = words > 110
    ? 'Длинновато: ' + words + ' слов. Google советует 80-100, дальше текст прячется под "ещё".'
    : words + ' слов. В рекомендуемых пределах.';

  var html = '<style>body{font-family:Arial,sans-serif;margin:0;padding:16px;font-size:13px;}'
    + 'label{display:block;font:600 12px Arial;color:#444;margin:14px 0 5px;}'
    + 'textarea,input{width:100%;font:13px/1.6 Arial;padding:10px;border:1px solid #ccc;'
    + 'border-radius:6px;box-sizing:border-box;}textarea{height:120px;}'
    + 'button{margin-top:8px;padding:8px 16px;border:0;border-radius:6px;background:#5C8B5F;'
    + 'color:#fff;font:600 13px Arial;cursor:pointer;}'
    + '.n{font:12px Arial;color:#666;margin:6px 0 0;}'
    + '.box{background:#F1F7F1;border-left:4px solid #5C8B5F;padding:12px 14px;'
    + 'border-radius:6px;margin-top:16px;line-height:1.6;}'
    + '.d{display:flex;gap:12px;}.d>div{flex:1;}'
    + 'ol{line-height:1.8;padding-left:18px;margin:10px 0 0;}</style>'

    + '<label>Текст поста</label>'
    + '<textarea id="t">' + esc(post.text) + '</textarea>'
    + '<button onclick="cp(\'t\',this)">Скопировать текст</button>'
    + '<p class="n">' + note + '</p>'

    + '<label>Ссылка для кнопки Learn more</label>'
    + '<input id="u" value="' + esc(post.cta) + '">'
    + '<button onclick="cp(\'u\',this)">Скопировать ссылку</button>'
    + '<p class="n">С метками UTM: в Google Analytics будет видно, сколько людей пришло из карточки.</p>'

    + '<div class="box"><b>Чтобы пост исчез сам через 30 дней</b><br>'
    + 'Выбирайте тип <b>Offer</b>, а не Update. Обычный Update висит полгода, '
    + 'а Offer Google снимает в указанную дату. Даты уже посчитаны:'
    + '<div class="d" style="margin-top:10px;">'
    + '<div><label style="margin:0 0 4px;">Start date</label>'
    + '<input id="d1" value="' + fmt(today) + '">'
    + '<button onclick="cp(\'d1\',this)">Копировать</button></div>'
    + '<div><label style="margin:0 0 4px;">End date</label>'
    + '<input id="d2" value="' + fmt(end) + '">'
    + '<button onclick="cp(\'d2\',this)">Копировать</button></div>'
    + '</div></div>'

    + '<ol><li>Найдите ProLife Home Care в Google или откройте Business Profile Manager</li>'
    + '<li>Add update, выберите <b>Offer</b></li>'
    + '<li>Заголовок: Now hiring HHA and PCA</li>'
    + '<li>Вставьте текст, даты начала и окончания</li>'
    + '<li>Кнопка Learn more, вставьте ссылку</li>'
    + '<li>Фото 1200x900: без картинки пост выглядит пустым</li></ol>'

    + '<script>function cp(id,b){var e=document.getElementById(id);e.select();'
    + 'document.execCommand("copy");var o=b.textContent;b.textContent="Скопировано";'
    + 'setTimeout(function(){b.textContent=o},1500);}<\/script>';

  SpreadsheetApp.getUi().showModalDialog(
    HtmlService.createHtmlOutput(html).setWidth(600).setHeight(640),
    'Пост для Google Business Profile'
  );
}

function esc(s) {
  return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;');
}

/* ── Автопостинг по расписанию ─────────────────────────────────────────── */

var AUTO_FN = 'postToTelegram';
var AUTO_MINUTES = 30;

/**
 * Строка готова к публикации, только если заполнены все обязательные поля.
 *
 * Без этой проверки автозапуск поймает строку на середине заполнения:
 * координатор вносит данные слева направо минуту-две, а триггер срабатывает
 * по своему расписанию. В канал уйдёт кейс без часов и языков, и отозвать
 * его уже нельзя.
 */
function isRowComplete(row) {
  for (var i = 0; i < COLUMNS.length; i++) {
    if (!COLUMNS[i].required) continue;
    if (String(row[i] || '').trim() === '') return false;
  }
  return true;
}

function autoPostingTrigger() {
  var all = ScriptApp.getProjectTriggers();
  for (var i = 0; i < all.length; i++) {
    if (all[i].getHandlerFunction() === AUTO_FN) return all[i];
  }
  return null;
}

function enableAutoPosting() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();

  if (!PropertiesService.getScriptProperties().getProperty('TG_TOKEN')) {
    ss.toast('Сначала: ProLife → Настроить Telegram', 'Бот не настроен', 10);
    return;
  }
  if (autoPostingTrigger()) {
    ss.toast('Автопостинг уже включён, каждые ' + AUTO_MINUTES + ' минут', 'Telegram', 8);
    return;
  }

  ScriptApp.newTrigger(AUTO_FN).timeBased().everyMinutes(AUTO_MINUTES).create();
  ss.toast('Включено. Новые кейсы будут уходить в канал каждые ' + AUTO_MINUTES + ' минут.',
           'Автопостинг', 10);
}

function disableAutoPosting() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var t = autoPostingTrigger();
  if (!t) { ss.toast('Автопостинг и так выключен', 'Telegram', 6); return; }
  ScriptApp.deleteTrigger(t);
  ss.toast('Выключено. Публиковать можно вручную через меню.', 'Автопостинг', 8);
}

function autoPostingStatus() {
  var on = !!autoPostingTrigger();
  var tg = !!PropertiesService.getScriptProperties().getProperty('TG_TOKEN');
  SpreadsheetApp.getUi().alert(
    'Состояние публикации',
    'Telegram-бот: ' + (tg ? 'подключён' : 'не настроен') + '\n' +
    'Автопостинг: ' + (on ? 'включён, каждые ' + AUTO_MINUTES + ' мин' : 'выключен') + '\n\n' +
    'Уходят только строки со статусом Open, заполненными обязательными полями\n' +
    'и пустой колонкой Posted.\n\n' +
    'Facebook и Reddit: только вручную. Meta закрыла Groups API в 2024,\n' +
    'на Reddit автопостинг вакансий ведёт к бану аккаунта.',
    SpreadsheetApp.getUi().ButtonSet.OK
  );
}

/**
 * Текст для ручной вставки: Facebook-группы, Reddit, WhatsApp.
 * Берётся строка, на которой стоит курсор.
 */
function copyPostText() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sh = ss.getActiveSheet();
  var row = sh.getActiveRange().getRow();

  if (sh.getName() !== SHEET || row < 2) {
    ss.toast('Встаньте на строку с кейсом на листе ' + SHEET, 'Подсказка', 8);
    return;
  }

  var data = sh.getRange(row, 1, 1, COLUMNS.length).getValues()[0];
  var text = buildPost(data, false);

  var html = '<style>body{font-family:Arial,sans-serif;margin:0;padding:14px;}'
    + 'textarea{width:100%;height:270px;font:13px/1.5 Arial;padding:10px;'
    + 'border:1px solid #ccc;border-radius:6px;box-sizing:border-box;}'
    + 'button{margin-top:10px;padding:9px 18px;border:0;border-radius:6px;'
    + 'background:#5C8B5F;color:#fff;font:600 14px Arial;cursor:pointer;}'
    + 'p{font:12px Arial;color:#666;margin:10px 0 0;}</style>'
    + '<textarea id="t">' + text.replace(/&/g,'&amp;').replace(/</g,'&lt;') + '</textarea>'
    + '<button onclick="var a=document.getElementById(\'t\');a.select();'
    + 'document.execCommand(\'copy\');this.textContent=\'Скопировано\';">Скопировать</button>'
    + '<p>Вставьте в группу Facebook, Reddit или WhatsApp. '
    + 'Автопостинг туда закрыт правилами площадок.</p>';

  SpreadsheetApp.getUi().showModalDialog(
    HtmlService.createHtmlOutput(html).setWidth(560).setHeight(420),
    'Текст объявления'
  );
}

function buildHelpSheet(ss) {
  var name = 'Как заполнять';
  var sh = ss.getSheetByName(name) || ss.insertSheet(name);
  if (!unlockSheet(sh)) return;   // лист уже есть и защищён: переписывать нечего
  sh.clear();

  var text = [
    ['Как добавить открытый кейс'],
    [''],
    ['1. Откройте лист Cases и идите на первую пустую строку.'],
    ['2. Начните с колонки Coordinator: выберите себя. Дата и статус Open проставятся сами.'],
    ['3. Дальше слева направо. В каждой колонке нажмите на ячейку и выберите значение из списка.'],
    ['4. Розовым подсвечены поля, которые ещё не заполнены. Пока есть розовое, кейс неполный.'],
    ['5. ZIP вводится вручную: ровно 5 цифр, не больше и не меньше. Другое значение таблица не примет.'],
    ['Если вставили ZIP копированием и ячейка покраснела, значит в нём не 5 цифр. Исправьте, иначе кейс на сайт не попадёт.'],
    ['6. Rate: обычно выбирается стандартная ставка из списка. Если кейс нестандартный, впишите своё значение руками.'],
    ['7. Notes пишется свободно и переводу не подлежит: на английской странице покажется как есть.'],
    [''],
    ['Когда кейс закрыт'],
    ['Поменяйте Status на Closed: кейс сразу пропадёт со страницы, строка станет серой.'],
    ['Status = Paused скрывает кейс временно, он ещё вернётся в работу.'],
    ['Когда закрытых строк накопится много: меню ProLife → Убрать закрытые кейсы в архив.'],
    ['Они переедут на лист Архив с датой переноса. Удалять руками не нужно.'],
    [''],
    ['Что видно на сайте'],
    ['Сайт обновляется в течение 5 минут после правки.'],
    ['Показываются только строки со статусом Open, корректным ZIP и заполненными обязательными полями.'],
    ['Пока в строке есть розовое, на сайте её нет. Это защита от полупустых карточек.'],
    ['Срочные кейсы поднимаются наверх списка.'],
    [''],
    ['Публикация в соцсетях'],
    ['Telegram: публикуется сам, каждые 30 минут. Ничего нажимать не нужно.'],
    ['Уходят только строки со статусом Open, где заполнены все обязательные поля.'],
    ['Пока строка неполная (есть розовое), в канал она не попадёт. Это защита от полуготовых объявлений.'],
    ['Нужно быстрее: меню ProLife → Опубликовать в Telegram сейчас.'],
    ['Facebook и Reddit: встаньте на строку, меню ProLife → Текст объявления. Скопируйте и вставьте сами.'],
    ['Google Business Profile: раз в неделю меню ProLife → Пост для Google Business Profile.'],
    ['Это сводка по всем открытым кейсам, а не отдельный пост на каждый: в карточке виден только самый свежий.'],
    ['Выбирайте тип Offer и вставляйте даты из окна: тогда пост снимется сам через 30 дней.'],
    ['Автопостинг в группы Facebook закрыт правилами Meta с 2024 года, обойти это нельзя.'],
    [''],
    ['Что защищено от правок'],
    ['Заголовки Cases, лист Public и эта инструкция закрыты: изменить их может только владелец таблицы.'],
    ['Заголовки трогать нельзя: по ним сайт находит колонки. Переименовали - список вакансий станет пустым.'],
    ['Public не редактируется руками: он собирается формулой и обновляется сам.'],
    ['Строки с кейсами не защищены: координаторы заполняют их как обычно.'],
    ['Выпадающие списки восстанавливаются сами. Если вставили значение копированием и список пропал,'],
    ['он вернётся при следующей правке этой ячейки.'],
    [''],
    ['Что уходит на сайт, а что остаётся внутри'],
    ['Наружу публикуется отдельный лист Public, а не Cases.'],
    ['На сайт уходят: тип, ZIP, район, часы, дни, смена, языки, пол сиделки, ставка, срочность.'],
    ['Остаются внутри: координатор, пол пациента, мобильность, примечание.'],
    ['Закрытые кейсы наружу не отдаются вообще, не только скрываются на сайте.'],
    [''],
    ['Чего в таблице быть не должно'],
    ['Даже во внутренних полях не пишите имя пациента, точный адрес и диагнозы.'],
    ['Состав публикуемых колонок может поменяться, а написанное однажды останется.'],
    ['ZIP и район дают сиделке всё, что нужно, чтобы понять, подходит ли ей поездка.']
  ];

  sh.getRange(1, 1, text.length, 1).setValues(text);
  sh.getRange(1, 1).setFontSize(14).setFontWeight('bold');
  sh.getRange(10, 1).setFontWeight('bold');
  sh.getRange(14, 1).setFontWeight('bold');
  sh.getRange(19, 1).setFontWeight('bold').setFontColor('#C0392B');
  sh.setColumnWidth(1, 700);
  sh.getRange(1, 1, text.length, 1).setWrap(true);
}

function colLetter(n) {
  var s = '';
  while (n > 0) {
    var m = (n - 1) % 26;
    s = String.fromCharCode(65 + m) + s;
    n = (n - m - 1) / 26;
  }
  return s;
}<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet type="text/xsl" href="//prolifehc.com/main-sitemap.xsl"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
	<sitemap>
		<loc>https://prolifehc.com/post-sitemap1.xml</loc>
		<lastmod>2026-06-05T21:17:24+00:00</lastmod>
	</sitemap>
	<sitemap>
		<loc>https://prolifehc.com/post-sitemap2.xml</loc>
		<lastmod>2026-06-05T21:17:21+00:00</lastmod>
	</sitemap>
	<sitemap>
		<loc>https://prolifehc.com/page-sitemap.xml</loc>
		<lastmod>2026-08-17T16:37:43+00:00</lastmod>
	</sitemap>
	<sitemap>
		<loc>https://prolifehc.com/category-sitemap.xml</loc>
		<lastmod>2026-05-30T01:33:58+00:00</lastmod>
	</sitemap>
	<sitemap>
		<loc>https://prolifehc.com/author-sitemap.xml</loc>
		<lastmod>2026-07-28T23:27:55+00:00</lastmod>
	</sitemap>
	<sitemap>
		<loc>https://prolifehc.com/video-sitemap.xml</loc>
		<lastmod>2026-03-09T20:15:10+00:00</lastmod>
	</sitemap>
</sitemapindex>
<!-- XML Sitemap generated by Rank Math SEO Plugin (c) Rank Math - rankmath.com -->