プログラム

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

2025年7月30日

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

この前のツイートで、こんな投稿をしました。

前置きとして雑ですが、今日はこの課題に対する対処をしていきたいと思います。

なんグラマ
なんグラマ

"なんちゃってプログラマ"の略でなんグラマです。プログラムを書いたり、ガジェットを紹介したりします。
お問い合わせ
プライバシーポリシー

▼以前作ったシフト希望のフォームを送信するプログラムはこちら▼

今回の仕様

①スプレッドシートの回答を確認する

  • 設定ファイルにある名前が、スプレッドシートの回答シートにすべて記述されているか確認する
  • 設定ファイルの名前がすべてない場合、②の仕様を実行する

②催促メッセージを送信する

  • 「まだシフト希望の回答をしていない方は回答してください。」とLINEのトークルームに送信する

実装

コードを書く場所

"src"ディレクトリに、新しく「remainde.gas」を作成し、ここにコードを書いていきます。


ホーム
├ config/
│   └ Config                   ← 設定値を管理するスプレッドシート
├ form/                        ← GAS が自動生成する Google フォームを格納
├ sheet/
│   └ shit                    ← フォーム回答が書き込まれるスプレッドシートを格納
└ src/
    ├  main.gs             ← メイン(フォームの送信)
    └ remainde.gs              ← 回答を催促する機能のgasファイル ★新規作成
コード作成場所
ファイル名変更

コード

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

// フォルダ検索フラグ
const ROOT_DIR = 0;
const SPECIFIC_DIR = 1;

/**
 * 未回答者がいる場合、催促のメッセージを送信する
 */
function remindUnanswered() {
  const cfg = loadConfig(); // 既存の設定読込を流用
  const ss = SpreadsheetApp.openById(cfg.spreadsheetId);

  // 回答シートを取得("Form Responses" or "フォームの回答" を検出)
  const responsesSheet = getResponsesSheet_(ss);

  // 回答済みの名前一覧を収集
  let respondedNames = [];
  if (responsesSheet) {
    const lastRow = responsesSheet.getLastRow();
    if (lastRow >= 2) {
      const headers = responsesSheet.getRange(1, 1, 1, responsesSheet.getLastColumn()).getValues()[0];
      const nameCol = findNameColumn_(headers); // 「名前」列の位置
      if (nameCol < 1) {
        // フォームの設問タイトルが「名前」になっていない等、想定外の場合は送信せず終了
        console.log('「名前」列が見つからないため、通知をスキップしました。');
        return;
      }
      respondedNames = responsesSheet
        .getRange(2, nameCol, lastRow - 1, 1)
        .getValues()
        .map(r => String(r[0]).trim())
        .filter(v => v);
    }
  }

  // 未回答者の有無だけ判定
  const respondedSet = new Set(respondedNames);
  const names = (cfg.names || []).map(n => String(n).trim()).filter(Boolean);
  const notAnswered = names.filter(n => !respondedSet.has(n));

  if (notAnswered.length === 0) {
    console.log('未回答者なし:通知をスキップしました。');
    return;
  }

  // 固定メッセージのみ送信(未回答者名は含めない)
  const message = 'まだシフト希望の回答をしていない方は回答してください。';
  sendTextViaLine_(message, cfg);
}

/**
 * config シートから A列(Key)/B列(Value) を読み込み、
 * オブジェクトを返す
 */
function loadConfig() {
  Logger.log("START FUNCTION: loadConfig()");
  const ss    = SpreadsheetApp.openById(CONFIG_SPREADSHEET_ID);
  const sheet = ss.getSheetByName(CONFIG_SHEET_NAME);
  const rows  = sheet.getRange('A2:B' + sheet.getLastRow()).getValues();
  const cfg   = {};
  rows.forEach(function(r) {
    const key = r[0], val = r[1];
    if (key) cfg[key] = val;
  });
  // names はコンマ区切り文字列 → 配列に
  cfg.names = cfg.names ? cfg.names.split(',').map(function(n){ return n.trim(); }) : [];
  Logger.log("cfg: %s", JSON.stringify(cfg));
  return cfg;
}

/**
 * 回答シート(Form Responses / フォームの回答)を返す
 */
function getResponsesSheet_(ss) {
  const sheets = ss.getSheets();
  for (const s of sheets) {
    const n = s.getName();
    if (n.indexOf('Form Responses') !== -1 || n.indexOf('フォームの回答') !== -1) {
      return s;
    }
  }
  return null;
}

/**
 * ヘッダー配列から「名前」列の 1-based インデックスを返す。見つからなければ 0
 */
function findNameColumn_(headers) {
  // 完全一致を優先
  let idx = headers.findIndex(h => String(h).trim() === '名前');
  if (idx >= 0) return idx + 1;

  // 念のため部分一致も最後にチェック(柔軟性確保・任意)
  idx = headers.findIndex(h => String(h).indexOf('名前') !== -1);
  if (idx >= 0) return idx + 1;

  return 0;
}

/**
 * LINEグループにテキストを送信
 */
function sendTextViaLine_(text, cfg) {
  const payload = {
    to: cfg.lineGroupId,
    messages: [{
      type: 'text',
      text
    }]
  };
  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
    headers: { Authorization: 'Bearer ' + cfg.accessToken },
    muteHttpExceptions: true
  };
  UrlFetchApp.fetch('https://api.line.me/v2/bot/message/push', options);
}

"スプレッドシートのファイルID"は、以下を参考にconfigファイルのURLから取得してください。

ID取得方法

動作確認

トリガーを設定する

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

トリガー設定方法①

「Add Trigger」を押す。

トリガー設定方法②

以下のように設定し、「Save」を押す。

  • Select type of time based trigger:「Week timer(毎週)」にする
  • Select day of week:「Every Friday(毎週金曜日)」にする
  • Select time of day:「5 pm to 6 pm(17時~18時の間)」にする

設定間隔はご自身の都合に合わせて調整してください

動作確認

コードの作成とトリガーを設定して迎えた、金曜日の17:00。例によって全員からの回答はありませんでした。LINEを確認してみると、グループトークに催促のメッセージが届いていました。

LINE画面

おすすめ記事

ブログ 雑談

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を使っていて、大きな不満はなかったのですがカメラだけはいまいち使い勝手がよく ...

-プログラム
-,