Защита диапазона ячеек в Google Sheets

Наши филиалы заказывают книги по таблице в Google Sheets с разграничением доступа по ячейкам. Таблица ранее была создана автоматически — Построение дополнительной таблицы в Excel.

После этого была перемещена в Google Sheets и написан скрипт для автоматической расстановки защиты данных.

/**
 * Проставляет права редактирования по филиалам
 */
function applyBranchEditors() {
  const ss = SpreadsheetApp.getActive();
  const sh = ss.getActiveSheet();

  // === Настройки ===
  const branches = ["256", "258", "259", "260", "261", "262", "263", "264", "265", "Юбилейный"];
  const ACCESS = {
    "256": "256@newmoslib.ru",
    "258": "258@newmoslib.ru",
    "259": "259@newmoslib.ru",
    "260": "260@newmoslib.ru",
    "261": "261@newmoslib.ru",
    "262": "262@newmoslib.ru",
    "263": "263@newmoslib.ru",
    "264": "264@newmoslib.ru",
    "265": "265@newmoslib.ru",
    "Юбилейный": "libyubileyny@newmoslib.ru",
  };

  const ADMINS = [
    Session.getEffectiveUser().getEmail(),
    "***@***.ru", // сюда можно вставить EMail адреса, которым будет доступно редактирование всего
  ];

  const BASE_COL = 8;   // H
  const START_COL = 10; // J
  const HEADER_ROW_1 = 1;
  const HEADER_ROW_2 = 2;
  const FIRST_DATA_ROW = 3;

  if (!branches.length) throw new Error("Список филиалов пуст.");

  // Последняя строка данных и строка итогов
  const lastDataRow = getLastDataRowByColumn_(sh, BASE_COL, FIRST_DATA_ROW);
  const totalRow = Math.max(FIRST_DATA_ROW, lastDataRow) + 1;

  // Границы блока по колонкам
  const lastBranchPairStart = START_COL + (branches.length - 1) * 2; // начало последней пары
  const lastBranchCol = lastBranchPairStart + 1;                      // «сумма» последнего филиала

  // === Сносим прошлые защиты, созданные этим скриптом ===
  const PROTECT_PREFIX = "[BRANCH-PROTECT]";
  sh.getProtections(SpreadsheetApp.ProtectionType.RANGE).forEach(p => {
    const d = p.getDescription();
    if (d && d.startsWith(PROTECT_PREFIX)) { try { p.remove(); } catch (e) {} }
  });

  // Важно: НЕ защищаем весь лист, только непересекающиеся диапазоны.
  // 1) Шапка (строки 1–2, весь блок филиалов) — только АДМИНЫ
  const headerRange = sh.getRange(
    HEADER_ROW_1,
    START_COL,
    HEADER_ROW_2 - HEADER_ROW_1 + 1,
    lastBranchCol - START_COL + 1
  );
  const protHeader = headerRange.protect();
  protHeader.setDescription(`${PROTECT_PREFIX} HEAD all-branches`);
  protHeader.setWarningOnly(false);
  setEditorsSafe_(protHeader, ADMINS);

  // 2) Для каждой «кол-во» — разрешаем филиалу (только строки данных);
  //    «сумма» (включая итог) и итог «кол-во» — только админы
  for (let i = 0; i < branches.length; i++) {
    const name = branches[i];
    const editorEmail = ACCESS[name];

    const qtyCol = START_COL + i * 2; // «кол-во»
    const sumCol = qtyCol + 1;        // «сумма»

    // «кол-во»: только строки данных (без итога!)
    if (editorEmail) {
      const qtyDataRange = sh.getRange(FIRST_DATA_ROW, qtyCol, (lastDataRow - FIRST_DATA_ROW + 1), 1);
      const protQty = qtyDataRange.protect();
      protQty.setDescription(`${PROTECT_PREFIX} QTY ${name}`);
      protQty.setWarningOnly(false);
      setEditorsSafe_(protQty, unique_(ADMINS.concat(editorEmail)));
    } else {
      Logger.log(`Нет e-mail для "${name}" — пропуск разрешённого диапазона "кол-во".`);
    }

    // «сумма»: данные + итог — только админы
    const sumAllRange = sh.getRange(FIRST_DATA_ROW, sumCol, (lastDataRow - FIRST_DATA_ROW + 1) + 1, 1);
    const protSum = sumAllRange.protect();
    protSum.setDescription(`${PROTECT_PREFIX} SUM ${name}`);
    protSum.setWarningOnly(false);
    setEditorsSafe_(protSum, ADMINS);

    // Итог «кол-во»: одна ячейка — только админы
    const qtyTotalCell = sh.getRange(totalRow, qtyCol, 1, 1);
    const protQtyTot = qtyTotalCell.protect();
    protQtyTot.setDescription(`${PROTECT_PREFIX} QTY_TOTAL ${name}`);
    protQtyTot.setWarningOnly(false);
    setEditorsSafe_(protQtyTot, ADMINS);
  }

  // 3) Остальной лист (вне блока филиалов) — только админы (три несмещающихся зоны)
  const maxRows = sh.getMaxRows();
  const maxCols = sh.getMaxColumns();

  if (START_COL > 1) {
    const leftRange = sh.getRange(1, 1, totalRow, START_COL - 1);
    const protLeft = leftRange.protect();
    protLeft.setDescription(`${PROTECT_PREFIX} OUTSIDE left`);
    protLeft.setWarningOnly(false);
    setEditorsSafe_(protLeft, ADMINS);
  }

  if (lastBranchCol < maxCols) {
    const rightRange = sh.getRange(1, lastBranchCol + 1, totalRow, maxCols - lastBranchCol);
    const protRight = rightRange.protect();
    protRight.setDescription(`${PROTECT_PREFIX} OUTSIDE right`);
    protRight.setWarningOnly(false);
    setEditorsSafe_(protRight, ADMINS);
  }

  if (totalRow < maxRows) {
    const belowRange = sh.getRange(totalRow + 1, 1, maxRows - totalRow, maxCols);
    const protBelow = belowRange.protect();
    protBelow.setDescription(`${PROTECT_PREFIX} OUTSIDE below`);
    protBelow.setWarningOnly(false);
    setEditorsSafe_(protBelow, ADMINS);
  }

  SpreadsheetApp.flush();
  SpreadsheetApp.getUi().alert('Права применены: «кол-во» — свой филиал; шапки, суммы, итоги и всё остальное — только админы.');
}

/** Последняя непустая строка по указанной колонке, начиная с FIRST_DATA_ROW */
function getLastDataRowByColumn_(sh, col, FIRST_DATA_ROW) {
  const lastRowSheet = sh.getLastRow();
  if (lastRowSheet < FIRST_DATA_ROW) return FIRST_DATA_ROW;
  const vals = sh.getRange(FIRST_DATA_ROW, col, lastRowSheet - FIRST_DATA_ROW + 1, 1).getValues();
  for (let i = vals.length - 1; i >= 0; i--) {
    if (vals[i][0] !== '' && vals[i][0] !== null) return FIRST_DATA_ROW + i;
  }
  return FIRST_DATA_ROW;
}

/** Устанавливает список редакторов для защиты, перезаписывая старых */
function setEditorsSafe_(prot, editors) {
  try { prot.setDomainEdit(false); } catch (e) { /* ignore */ }

  try {
    const current = prot.getEditors();
    if (current && current.length) prot.removeEditors(current);
  } catch (e) { /* ignore */ }

  try {
    prot.addEditors(editors);
  } catch (e) {
    editors.forEach(email => { try { prot.addEditor(email); } catch (e2) {} });
  }
}

/** Удаляет дубли из массива */
function unique_(arr) {
  return Array.from(new Set(arr));
}