プログラム

【LINE】シフト希望の回答を整理する(GAS)

2025年7月22日

当サイトはアフィリエイト広告による収入を得ています

先日の記事で、LINEのグループトークでシフト希望の回答をするGoogleフォームを自動送信する方法をまとめました。

プログラム

2025/7/28

【LINE】GASでシフト希望の募集を自動化する(無料)

接客業で働き始めて早3年。3年経ったということで、最近はバイトの方のシフト管理を任せられることになりました。シフト管理をすることで見えてきた改善できそうなことに対して今回は取り組んでいきたいと思います。 こんな方におすすめの記事 お金をかけずにシフト募集を自動化したい方 GASを勉強したい方 現在の実態と目標 現在はバイトの方とのグループLINEで週一回シフト希望を聞いて、回答をもとにシフトを考え共有しています。しかし、これは時間の無駄でかつめんどくさいんです。なので、これらの作業を自動化していきたいと思 ...

これで晴れて、シフト希望の募集は自動化されるようになりました。そして、回答はスプレッドシートに記録されるのですが、

現在のシフト希望の回答画面
なんグラマ
なんグラマ

これ、見にくい…

ということで、今回はフォームの回答を自動で見やすく整理する機能を実装していきたいと思います。

既存機能のおさらい

今回の仕様を詳しく説明する前に、これまでの機能などをおさらいします。

プロジェクト構成

既存機能では、main.gasにLINEのグループトークにGoogleフォームを送信する処理を記述しています 。


ホーム
├ config/
│   └ Config                   ← 設定値を管理するスプレッドシート
├ form/                        ← GAS が自動生成する Google フォームを格納
├ sheet/
│   └ shit                    ← フォーム回答が書き込まれるスプレッドシートを格納
└ src/
    └ main.gs        ← 全ロジック(GAS スクリプト本体)

既存機能

  • 毎週水曜日にLINEのグループトークにシフト希望の回答をするGoogleフォームを送信する
  • Googleフォームの回答を受けるとファイル名"shift”のスプレッドシートに「可能」「不可」のどちらかで回答を記録する

今回の仕様

①回答を記録するシートの自動生成

仕様①シートの自動作成
  • 回答を記録するシートは、年(例:2025)をそのままシート名として作成する
  • A列の2行目以降に、その年1年分の日付を記載する

②ヘッダーに名前を自動記載

仕様②名前の自動記載
  • 設定ファイルから名前を取得し、B列以降に記載する
  • 既にシートに名前がある場合は、新たに記述はしない
  • 設定ファイルに記述のない名前がシートにある場合、その名前のある列を削除する

③出勤可否を〇×で記載

仕様③出勤可否を〇×記載
  • 出勤可否を可能なら"〇"、不可なら"×"と記載する

実装

コードを書く場所

スプレッドシート"shift"を開き、「Extensions」→「App Script」の順にクリックして、開いた場所にコードを記述してください。

GASエディタの開き方

コード

// 設定を記述したスプレッドシートのファイルID
const CONFIG_SPREADSHEET_ID = 'スプレッドシートのファイルID';
// 設定を管理するシート名
const CONFIG_SHEET_NAME = 'config';

// 翌週の日付をためる配列
let weekDates = [];
let weekDateObjects = [];

/**
 * フォーム回答トリガーで実行されるメイン関数
 * e.values: [タイムスタンプ, 回答者名, 日付1の回答, 日付2の回答, …]
 */
function updateShifts(e) {
  // 翌週の日付(月曜~日曜)を取得
  getWeekDates();

  // 名前を取得
  const validNames = loadConfigNames();
  const respondentName = e.values[1];
  const nameIndex = validNames.indexOf(respondentName);
  if (nameIndex < 0) {
    throw new Error(`config シートに未登録の名前: ${respondentName}`);
  }
  const targetColumn   = nameIndex + 2;  // B列=2, C列=3,…

  // 回答を記録するスプレッドシートを開く
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const processedYears = {};

  let firstSheet, firstRow;

  weekDates.forEach((label, i) => {
    const dateObj = weekDateObjects[i];
    const year    = dateObj.getFullYear();

    // シートがない場合
    if (!processedYears[year]) {
      // その年でシートを作成
      ensureYearSheet(year);
      const sheet = ss.getSheetByName(String(year));

      // GAS実行時に config ファイルにある名前をすべて記述する
      sheet.getRange(1, 2, 1, validNames.length).setValues([validNames]);

      // 設定ファイルにない名前の列を削除する
      const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
      for (let col = headers.length; col >= 2; col--) {
        const h = headers[col - 1];
        if (h && validNames.indexOf(h) === -1) {
          sheet.deleteColumn(col);
        }
      }

      // ヘッダーに名前がない場合は追加する
      const existing = sheet.getRange(1, targetColumn).getValue();
      if (existing !== respondentName) {
        sheet.getRange(1, targetColumn).setValue(respondentName);
      }

      // 日付ラベルの配列を取得(A2~最終行)
      const dateLabels = sheet
        .getRange(2, 1, sheet.getLastRow() - 1, 1)
        .getValues()
        .map(r => r[0]);

      processedYears[year] = { sheet, dateLabels };
    }

    // 出勤可否(○・×)を書き込む
    const { sheet, dateLabels } = processedYears[year];
    const idxInSheet = dateLabels.indexOf(label);
    if (idxInSheet < 0) {
      throw new Error(`シート ${year} に日付ラベル「${label}」が見つかりません`);
    }
    const row = idxInSheet + 2;

    const response = e.values[i + 2];  // e.values[2] が日付1の回答
    const mark     = (response === '可能') ? '○' : '×';
    sheet.getRange(row, targetColumn).setValue(mark);

    // 最初の書き込みではアクティブセル用に保持
    if (i === 0) {
      firstSheet = sheet;
      firstRow   = row;
    }
  });

  // 月曜行をアクティブに設定
  if (firstSheet && firstRow) {
    firstSheet.setActiveRange(firstSheet.getRange(firstRow, 1));
  }
}

/**
 * 翌週月曜~日曜の日付ラベル('M/D(曜)')と Date オブジェクトを取得
 */
function getWeekDates() {
  weekDates = [];
  weekDateObjects = [];

  const today = new Date();
  const weekdayIndex = today.getDay();
  const offsetToMon = (8 - weekdayIndex) % 7;
  const nextMonday = new Date(
    today.getFullYear(),
    today.getMonth(),
    today.getDate() + offsetToMon
  );
  const options = { weekday:'short', month:'numeric', day:'numeric' };

  for (let i = 0; i < 7; i++) {
    const d = new Date(
      nextMonday.getFullYear(),
      nextMonday.getMonth(),
      nextMonday.getDate() + i
    );
    weekDateObjects.push(new Date(d));  // Date オブジェクト保持
    weekDates.push(d.toLocaleDateString('ja-JP', options));
  }
}

/**
 * config シートから名前を取得して返す
 * @returns {string[]} ['佐藤','鈴木',…]
 */
function loadConfigNames() {
  const cfgSs = SpreadsheetApp.openById(CONFIG_SPREADSHEET_ID);
  const cfgSheet = cfgSs.getSheetByName(CONFIG_SHEET_NAME);
  const data = cfgSheet.getDataRange().getValues();
  for (let i = 0; i < data.length; i++) {
    if (data[i][0] === 'names') {
      return String(data[i][1])
        .split(',')
        .map(v => v.trim())
        .filter(v => v);
    }
  }
  throw new Error('config シートに "names" の行が見つかりません');
}

/**
 * 年ごとのシートを確保し、存在しなければ新規作成してA1 に年、A2~ にその年の全日付(曜付き)を投入する
 */
function ensureYearSheet(year) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const name = String(year);
  let sheet = ss.getSheetByName(name);
  if (sheet) return;  // 既存なら何もしない

  sheet = ss.insertSheet(name);
  // A1:年を表示
  sheet.getRange(1, 1).setValue(year);
  // A2以降:その年の全日付
  const start = new Date(year, 0, 1);
  const end = new Date(year + 1, 0, 1);
  const options = { weekday:'short', month:'numeric', day:'numeric' };
  let row = 2;
  for (let d = new Date(start); d < end; d.setDate(d.getDate() + 1)) {
    sheet.getRange(row++, 1)
         .setValue(d.toLocaleDateString('ja-JP', options));
  }

  // シート作成時に、すべてのセルを中央揃え
  sheet.getRange(1, 1, sheet.getMaxRows(), sheet.getMaxColumns()).setHorizontalAlignment('center');
  // シート作成時に、1行目を固定
  sheet.setFrozenRows(1);
}

/**
 * テスト関数: testUpdateShiftsYearEnd
 *
 * 概要:
 * 年末年始をまたぐ週(例:2025-12-29(月)~2026-01-04(日))のシフト反映ロジックを検証します。
 * weekDates と weekDateObjects をスタブし、updateShifts() を実行することで、
 * 2025 年タブおよび 2026 年タブに
 * 各日付行の「佐藤」列へ正しく “○” が書き込まれるかをチェックします。
 *
 * 実行方法:
 * 1. Google Apps Script エディタを開きます。
 * 2. エディタ上部の関数一覧ドロップダウンから
 *    “testUpdateShiftsYearEnd” を選択します。
 * 3. ▶︎(実行)ボタンをクリックして関数を実行します。
 * 4. スプレッドシートに戻り、"2025" シートと "2026" シートの
 *    12/29~1/4 の各行・「佐藤」列に “○” が入っていることを確認してください。
 */
function testUpdateShiftsYearEnd() {
  // —————— step1: getWeekDatesをコピー ——————
  const originalGetWeekDates = getWeekDates;

  // —————— step2: テスト用に weekDates を固定する stub を差し替え ——————
  getWeekDates = function() {
    weekDates = [
      '12/29(月)',
      '12/30(火)',
      '12/31(水)',
      '1/1(木)',
      '1/2(金)',
      '1/3(土)',
      '1/4(日)'
    ];
    weekDateObjects = [
      new Date(2025, 11, 29),
      new Date(2025, 11, 30),
      new Date(2025, 11, 31),
      new Date(2026,  0,  1),
      new Date(2026,  0,  2),
      new Date(2026,  0,  3),
      new Date(2026,  0,  4)
    ];
  };

  // —————— step3: ダミー回答イベントを作成 ——————
  const fakeEvent = {
    values: [
      new Date().toISOString(), // タイムスタンプ
      '佐藤',                    // 回答者名
      '可能','可能','可能','可能','可能','可能','可能'
    ]
  };

  // —————— step4: メイン処理を呼び出し ——————
  updateShifts(fakeEvent);

  // —————— step5: getWeekDates を元に戻す ——————
  getWeekDates = originalGetWeekDates;

  Logger.log('テスト完了:2025年タブ・2026年タブに○が入っているか確認してください');
}

動作確認

トリガーを設定する

GASエディタ画面から「Triggers」をクリックする。

スプレッドシート GASエディタ画面

"Select event type"の「On form submit」をクリックし、「Save」をクリックする。

トリガー追加画面

動作確認

①LINEで送られているフォームをクリックし、回答する。

LINE画面
Googleフォーム画面

②"shift"スプレッドシートを開いて回答の内容が正しく反映されているか確認する。

スプレッドシート回答画面

おすすめ記事

ブログ 雑談

2025/8/2

僕の経歴とブログを始めた経緯

このブログを運営している"なんグラマ"といいます。「なんちゃってプログラマ」の略です。 本業は接客業をしていて、日々お客様と接しています。接客業も好きでこの仕事をしているのですが、他にもいくつか好きなこと(モノ)があります。 それは、 プログラミング ガジェット スイーツ です。 当ブログでは、この3つのジャンルを中心に投稿しています。この投稿では、自己紹介として私の人生の一部を話す中で、ブログを始めた理由も話していきます。 【幼稚園~中学時代】将来の夢は列車の運転士だった 幼稚園くらいの頃から列車が好き ...

オーディオ

2025/7/12

【WH-1000XM6】人生初のヘッドホン購入

自宅でパソコンを触るときはよくワイヤレスイヤホンで作業用のBGMを流しています。ただ、イヤホンは長時間つけっぱなしにすると耳が痛くなってきます。なので、耳の痛くなりにくいヘッドホンが欲しいと思い、ヘッドホン探しをしていたのです。ちょうどその時、SONYのWH-1000XM6が発表されました。価格は約6万円と高めですが、思い切って奮発しました。 いざ、開封 箱を開けると、ケースと取り扱い説明書がでてきました。ケースを開けると、 ヘッドホン本体とケーブルが出てきました。ヘッドホンはブラックを選択しました。高級 ...

オーディオ

2025/7/23

【Soundcore Liberty 5】1.5万円の最高イヤホン

安くて高性能と話題になっていたSoundCore Liberty 4の後継機がついに発売されました。しばらくイヤホンを買っていなかったので、これを機に買ってみることにしました。 Anker Soundcore Liberty 5 posted with カエレバ 楽天市場 Amazon Soundcore Liberty 5のスペック 詳細なスペック 詳細なスペックは下記です。「これだけの機能があって、この値段でいいの!?」という具合です。 Soundcore Liberty 5Soundcore Lib ...

プログラム

2025/7/28

【LINE】GASでシフト希望の募集を自動化する(無料)

接客業で働き始めて早3年。3年経ったということで、最近はバイトの方のシフト管理を任せられることになりました。シフト管理をすることで見えてきた改善できそうなことに対して今回は取り組んでいきたいと思います。 こんな方におすすめの記事 お金をかけずにシフト募集を自動化したい方 GASを勉強したい方 現在の実態と目標 現在はバイトの方とのグループLINEで週一回シフト希望を聞いて、回答をもとにシフトを考え共有しています。しかし、これは時間の無駄でかつめんどくさいんです。なので、これらの作業を自動化していきたいと思 ...

スマホ

2025/7/12

【Galaxy S25 Ultraレビュー】すべてが最高峰。でも高い

私はスマホにはストレスのない最高のものを使いたいと思っています。なぜなら、毎日触れるから。今までGalaxy Z Fold5を使っていて、大きな不満はなかったのですがカメラだけはいまいち使い勝手がよくないなと思っていました。(十分綺麗なのに)そしてこの、Galaxy Z Fold5はドコモのいつでもカエドキプログラムで借りていて、9月に返却しないといけません。どうせ9月には新しい端末を購入するから「予約特典のある早いうちに買った方がいいじゃね?」と思って買ったGalaxy S25 Ultraのレビューをし ...

ガジェット

2025/7/12

【便利。だけど高い】NFCボタン搭載のPITAKAのスマホケース

突然ですが、今までGalaxy S25 Ultraで使っていたスマホケースがこれです ↓ このケースは前面は保護されていますが、カメラ部分が全然保護されない。。という不満を持っていました。20万も出して買ったスマホのレンズに傷がついたらかなりショックだなと思っていた矢先、InstagramでPITAKAのスマホケースの広告が流れてきました。 カメラ部分もしっかり保護してあります。そして、何やら左側面の3つのボタンにショートカットを割り当てられるそうです。任意のアプリが割り当てられそうで、「絶対便利じゃん! ...

  • この記事を書いた人

なんグラマ

福岡県在住の23歳。高校を卒業後2年くらいシステムエンジニアをして、接客業に転職。プログラムを書くことが今でも好きで、ChatGPTなどを活用して仕事やプライベートを楽する方法を探しています。ガジェット、甘い食べ物が大好き。

こんな記事もいかがですか?

1

自宅でパソコンを触るときはよくワイヤレスイヤホンで作業用のBGMを流しています。ただ、イヤホンは長時間つけっぱなしにすると耳が痛くなってきます。なので、耳の痛くなりにくいヘッドホンが欲しいと思い、ヘッ ...

2

安くて高性能と話題になっていたSoundCore Liberty 4の後継機がついに発売されました。しばらくイヤホンを買っていなかったので、これを機に買ってみることにしました。 Anker Sound ...

3

私はスマホにはストレスのない最高のものを使いたいと思っています。なぜなら、毎日触れるから。今までGalaxy Z Fold5を使っていて、大きな不満はなかったのですがカメラだけはいまいち使い勝手がよく ...

-プログラム
-,