【Google Apps Script】Google Formsで会議出欠確認アプリを作って自動運用を目指す

技術

面倒な出欠確認のまとめ作業

システム クラフトとは別に、私が所属している会社では毎月第3金曜日に社員総会が社員全員参加で行われます。
どこの会社でも多かれ少なかれそういったことはあると思いますが、当然都合がつかなくて欠席せざるを得ない社員もいるわけです。
欠席する場合、現在の運用では上長の許可を取ってチームごとに、誰が出席するのか、欠席するのかをまとめて報告するということになっています。

で、私はまとめて報告するってところを担当しているんですが、これが地味に面倒くさい。

  1. 月初に全員へChatで会議の日程を連絡。
  2. 会議の3日前までに反応のない人には、再度連絡して出欠を確認。
  3. 集まった出欠を決まったフォーマットにまとめて報告する。

たったこれだけの何が面倒くさいのかといえば

  • 出欠の報告はバラバラにChatや電話で受けてメモしておかないといけない。
  • そして誰が連絡漏れなのかを確認して、催促しないといけない。
  • 最終的に報告用のフォーマットにまとめ直して報告。
  • また月が替われば新たに次の社員総会のお知らせをしないといけない。

この面倒をGoogle FormsGoogle Apps Script(GAS)で解決できないか考えてみました。

出欠報告はGoogle Formsで一括管理

各自に出欠を報告してもらうのはGoogle Formsを使えば簡単にできそうです。
必要なのは「出席/欠席の選択肢」と「欠席理由の入力項目(欠席時のみ表示)」の2つ。

メールアドレスがわかれば、いちいち名前は入力してもらう必要がないので、入力項目としては作りませんでした。※
その代わりGoogle Formsの設定で「メールアドレスを収集する」を “ON” にしておきます。

これ以上の詳細は割愛しますが、こんな感じの画面になりました

※会社ではGoogle Workspaceを使っていて、Workspaceの管理アカウントでのログインを要求するようにGoogle Formsで設定できます。メールアドレスがわかれば誰のアカウントなのかはわかりますね。

GASで自動運用する

Google Formsで出欠の確認はアプリ化できました。
次はこのアプリの自動運用を目指します。
自動運用に必要なのは下記の通りです。

  1. 毎月1日に次回の日程と出欠の登録を依頼するメッセージをGoogleChatへ投稿
  2. 回答期限当日(会議の3日前)に出欠未登録の社員宛に連絡
  3. 回答期限翌日(会議の2日前)に回答を締め切り、回答内容をまとめて自分宛に連絡

だいたいこんな感じでしょうか。

最終的に自分宛てに届いた「回答内容のまとめ」をそのまま会社へ報告することにします。
会社への報告はあえて手動にして、報告前の最終確認ができるようにしました。

連絡はすべてGoogleChatのWebhookにします。
少し前の記事で紹介したPythonで実装したのとだいたい同じことをGASでやればいいだけなので簡単です。

【Python】Webhookで簡単にGoogle Chatにメッセージを送る
Webhookって? Webhookとはアプリケーションで発生したイベントをリアルタイムに外部へ通知する概念のことです。Google ChatにはIncomming Webhookと呼ばれる、URLを公開して外部からメッセージが投稿できる仕...

まずはGoogle Formsからスクリプトエディタを開きます。

スクリプトエディタが開いたら、早速Chatへメッセージを投げる仕組みから作りましょう。

function chatMessage(url, message) {
  let param = {
    'method': 'POST',
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8'
    },
    'payload':`{\"text\":\"${message}\"}`
  };
  UrlFetchApp.fetch(url, param);
}

詳しい説明は以前の記事を参考にしてください。

続けて会議の開催日、つまり当月の第三金曜日を取得する処理を作ります。

function get3rdFriday() {
  let d = new Date();
  d.setDate(1);                 // ここで d は今月1日
  d.setDate(6 - d.getDay());    // ここで d は第一金曜日になった
  d.setDate(d.getDate() + 14);  // ここで d は第三金曜日になった
  return d;
}

ロジックとしては簡単で「1日から数えて最初の金曜日を見つけて、その14日後が第三金曜日」ってだけです。

ここまで出来たらいよいよ毎月1日に動かす処理を実装します。

openForm:フォームをリセットして公開する

const SPREAD_SHEET_ID = "XXXXXXXXXXXXXXXXXXXXXXXXX";
const CHAT_URL = "https://chat.googleapis.com/v1/spaces/XXXXXXXXXX/messages?key=XXXXXXXXXXXXXX-XXXXXXXXXXXXXX&token=XXXXXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXX%3D";

function openForm() {
  // 会議開催日を取得
  const targetDate = get3rdFriday();
  const targetDateStr = Utilities.formatDate(targetDate, 'Asia/Tokyo', "M月d日") + "(金)";
  // 出欠確認の締め切り日(会議の3日前)を取得
  const closeDate = new Date(targetDate);
  closeDate.setDate(closeDate.getDate() - 3);
  const closeDateStr = Utilities.formatDate(closeDate, 'Asia/Tokyo', "M月d日") + "(火)";
  // フォームを取得
  const form = FormApp.getActiveForm();
  // 先月の全員の回答をクリアして、「回答締め切り」を解除する
  form.deleteAllResponses();
  form.setAcceptingResponses(true);
  // フォームのタイトルと説明を更新
  form.setTitle(Utilities.formatDate(targetDate, 'Asia/Tokyo', "y年M月") + " 社員総会出欠確認");
  form.setDescription(
    `次回の社員総会は${targetDateStr}です。${closeDateStr}までに登録してください。\n` +
    "\n" +
    "全社員の出席予定はこちらで確認できます。\n" +
    `https://docs.google.com/spreadsheets/d/${SPREAD_SHEET_ID}`
  );
  // グーグルチャットで全員へ連絡
  chatMessage(
    CHAT_URL,
    `<users/all> \n次回の社員総会は${targetDateStr}です。${closeDateStr}までに下記のフォームより出欠予定を登録してください\n${form.getPublishedUrl()}`
  );
}

プログラム内のコメントを見れば何をしているか大体わかると思います。
フォームをリセットして、GoogleChatへメッセージを投げているだけです。
説明の必要があるとすれば2点だけ。

1つ目はフォームの説明にGoogle SpreadSheetへのリンクを載せていることです。
せっかくなんで、回答状況を全員で共有できるといいかなと思って、回答があるたびにSpreadSheetへ反映させようと思います。
実装方法は後程説明します。

2つ目はGoogle Chatへのメッセージに<users/all>というキーワードが含まれていることです。
こうすることでChatルームの全員にメンションすることができます。

ところで、このopenFormという処理は後でもう少しだけ処理を付け加えます。
期限の当日と翌日に処理を実行するためのトリガー登録を実装する予定です。

updateList:SpreadSheetの出欠状況一覧を更新する

次は回答者が回答を送信するたびに実行する処理を実装します。

まずは現在の全員の回答状況を取得する処理を作ります。
これはこの後のロジックで使いまわしできるようにgetAnswersという関数にしました。

Google Formsから回答を読み取るインターフェイスはこんな感じになっています。
この辺りを使って、扱いやすいようにデータを作ります。

const responses = form.getResponses();   // 全員の回答を取得
const resp = responses[0];               // 最初の回答者の回答を取得
const email = resp.getRespondentEmail(); // 回答者のメールアドレスを取得
const items = resp.getItemResponses();   // 回答を配列で取得
const ABSENCE = 0;
const OFFICE = 1;
const HOME = 2;

function getAnswers() {
  const form = FormApp.getActiveForm();
  // 現在集まっている回答を取得
  const responses = form.getResponses();
  // 1人ずつ出欠状況をanswersにセット
  const answers = {};
  for (let resp of responses) {
    let items = resp.getItemResponses();
    // 出欠状況を取得
    let participation = ABSENCE;
    if (items[0].getResponse().startsWith("はい、本社")) {
      participation = OFFICE;
    } else if (items[0].getResponse().startsWith("はい、自宅")) {
      participation = HOME;
    }
    answers[resp.getRespondentEmail()] = {
      participation: participation,
      reason: items.length > 1 ? items[1].getResponse() : undefined
    };
  }

  return answers;
}

出欠状況としては「欠席(ABSENCE)」、「会社に来て出席(OFFICE)」、「自宅からオンラインで出席(HOME)」の3Stateにしました。
欠席の場合は欠席理由が入力されているはずなので、その内容も取得しておきます。

取得した結果はanswersという変数名の連想配列にセットしています。
連想配列のキーはメールアドレスです。

では、この取得した回答状況をスプレッドシートに反映する部分を作りましょう。

const USERS = {
  "taro-yamada@systemcraft.biz": "山田太郎",
  "hanako-itou@systemcraft.biz": "伊藤花子",
  "hiroshi-satou@systemcraft.biz": "佐藤博"
};

function updateList() {
  // フォームから回答状況を取得
  const answers = getAnswers();
  // スプレッドシートを開いてシートを取得(なければ作成)
  const spreadSheet = SpreadsheetApp.openById(SPREAD_SHEET_ID);
  const sheetName = Utilities.formatDate(new Date(), 'Asia/Tokyo', "y年M月")
  let sheet = spreadSheet.getSheetByName(sheetName);
  if (sheet == null) {
    sheet = spreadSheet.insertSheet(0);
    sheet.setName(sheetName);
  }
  // 現在の内容を一旦クリア
  sheet.clear();
  // ヘッダを出力
  let row = 1;
  sheet.getRange(row, 1, 1, 3).setValues([["【名前】", "【参加】", "【備考】"]]);
  row++;
  // 現在の回答内容を出力
  for (let email in USERS) {
    let user = USERS[email];
    sheet.getRange(row,1).setValue(user);
    if (answers[email]) {
      sheet.getRange(row,2).setValue(answers[email].participation == OFFICE ? "本社" : answers[email].participation == HOME ? "自宅" : "×");
      sheet.getRange(row,3).setValue(answers[email].reason == undefined ? "" : answers[email].reason);
    }
    row++;
  }
}

USERSという連想配列でメールアドレスと名前の関連付けを定義しています。

getAnswersで取得した回答内容を名前と共にスプレッドシートへ出力しているわけです。
ちなみに未回答の人は名前だけ出力して、「参加」の部分は空白のままになるようにしてみました。

結果はこんな感じのスプレッドシートになりました。

alertion:未回答者に警告する

続けて、回答期限当日(会議の3日前)に走る処理を作りましょう。

function alertion() {
  // 会議開催日を取得
  const targetDate = get3rdFriday();
  const targetDateStr = Utilities.formatDate(targetDate, 'Asia/Tokyo', "M月d日") + "(金)";
  // フォームから回答状況を取得
  const answers = getAnswers();
  // 未回答者を検索
  let unanswered = [];
  for (let email in USERS) {
    if (answers[email] == undefined) {
      unanswered.push(USERS[email]);
    }
  }
  // 未回答者がいる場合はGoogleChatへメッセージを投稿する
  if (unanswered.length > 0) {
    chatMessage(
      CHAT_URL,
      `${targetDateStr}の社員総会への出欠予定登録は本日が期限となっています。未登録の方は下記のフォームより出欠予定を登録してください\n` +
      `【未回答者】\n` +
      `${unanswered.join("、")}\n` +
      `${FormApp.getActiveForm().getPublishedUrl()}`
    );
  }
}

未回答者の名前と共に登録をお願いするメッセージをGoogleChatへ投稿しています。

今回は未回答者が0人の場合、メッセージを送らないように作ってみましたが、メッセージの内容を変えてもうすぐ社員総会であることをリマインドしてもいいかもしれませんね。

closeForm:回答を締め切って回答内容をまとめる

最後は回答期限翌日(会議の2日前)に実行する処理の実装です。

const CHAT_URL_SELF = "https://chat.googleapis.com/v1/spaces/YYYYYYYYYYYY/messages?key=YYYYYYYYYYYYYY-YYYYYYYYYYYYYY&token=YYYYYYYYYYYYYYYY-YYYYYYYYYYYYYYYYYYYYY%3D";

function closeForm() {
  const form = FormApp.getActiveForm();
  // 「回答締め切り」
  form.setAcceptingResponses(false);
  // フォームから回答状況を取得
  const answers = getAnswers();
  let office = [];
  let home = [];
  let absence = [];
  let unknown = [];
  for (let email in USERS) {
    let user = USERS[email];
    if (answers[email] == undefined) {
      unknown.push(user);
    } else {
      switch (answers[email].participation) {
        case OFFICE:
          office.push(user);
          break;
        case HOME:
          home.push(user);
          break;
        default:
          absence.push(`${user}:「${answers[email.reason]}」`);
      }
    }
  }
  // 集計結果をチャット
  let msg = `【${(new Date().getMonth() + 1)}月社員総会】\n`
  msg += "Aチームの出欠予定です。\n";
  msg += "\n";
  msg += `本社参加:${office.length == 0 ? "なし" : office.join('、')}\n`;
  msg += `在宅参加:${home.length == 0 ? "なし" : home.join('、')}\n`;
  msg += `欠席  :${absence.length == 0 ? "なし" : absence.join('\n')}`;
  if (unknown.length > 0) {
    msg += `\n未確認 :${unknown.join('、')}\n`;
  }
  chatMessage(CHAT_URL_SELF, msg);
}

form.setAcceptingResponses(false)で回答を締め切ることができます。
回答が締め切られると、誰も新たな回答や自分の回答の編集ができなくなります。

フォームの回答状況を取得して報告用のフォーマットにまとめて、自分自身に送信しています。
全員への通知用のGoogleChatのURL(CHAT_URL)とは別に自分だけが参加しているチャットルーム(CHAT_URL_SELF)をメッセージの宛先にしているわけです。

トリガーで処理の実行をスケジュールする

プログラムが作れたら、後はスケジュール通り処理が動き出すように設定するだけです。
プログラムのスケジュールにはトリガーという機能を使います。

まずは毎月1日に動くopenFormと、回答が送信されるたびに動くupdateListのトリガーを設定します。
スクリプトエディターの左のメニューから「トリガー」を選択してトリガー設定画面を開きます。

トリガーの追加ボタンから、それぞれ下記のように設定します。

実行する関数を選択openForm
デプロイ時に実行Head
イベントのソースを選択時間主導型
時間ベースのトリガーのタイプを選択 月ベースのタイマー
日を選択1日
時刻を選択午前10時~11時
エラー通知設定今すぐ通知を受け取る
実行する関数を選択updateList
デプロイ時に実行Head
イベントのソースを選択フォームから
イベントの種類を選択フォーム送信時
エラー通知設定1週間おきに通知を受け取る

これで毎月1日の10:00頃にopenFormが動き、回答の送信が行われるたびにupdateListが動くようになりました。

次に回答期限当日と翌日に処理を走らせるトリガーを設定します。

回答期限は毎月何日になるのか決まってないので、単純にトリガー設定画面で設定することができません。
そこで毎月1日に動くopenFormという関数の中で、プログラムからトリガーを設定する方法を使います。

function openForm() {
  // 会議開催日を取得
  const targetDate = get3rdFriday();
  const targetDateStr = Utilities.formatDate(targetDate, 'Asia/Tokyo', "M月d日") + "(金)";
  // 出欠確認の締め切り日(会議の3日前)を取得
  const closeDate = new Date(targetDate);
  closeDate.setDate(closeDate.getDate() - 3);
  const closeDateStr = Utilities.formatDate(closeDate, 'Asia/Tokyo', "M月d日") + "(火)";

   ・
   ・
   ・
  (中略)
   ・
   ・
   ・

  // トリガーを一旦クリア
  var triggers = ScriptApp.getProjectTriggers();
  for(var i=0; i < triggers.length; i++) {
    if (triggers[i].getHandlerFunction() != "openForm" && triggers[i].getHandlerFunction() != "updateList") {
      ScriptApp.deleteTrigger(triggers[i]);
    }
  }
  // 提出期限の当日10:00でトリガー設定
  closeDate.setHours(10);
  closeDate.setMinutes(0);
  closeDate.setSeconds(0);
  ScriptApp.newTrigger('alertion').timeBased().at(closeDate).create();
  // 提出期限の翌日5:00でトリガー設定
  closeDate.setDate(closeDate.getDate() + 1);
  closeDate.setHours(5);
  ScriptApp.newTrigger('closeForm').timeBased().at(closeDate).create();
}

トリガーを登録する前に先月登録していたトリガーを一旦クリアします。
トリガー設定画面で登録したopenFormとupdateListのトリガーを一緒に消してしまわないように注意しましょう。

これでopenFormが実行されると期限当日の朝10:00頃にalertionという処理が走り、翌日の5:00頃にcloseFormが走るようになりました。

以上の設定と実装で会議出欠アプリの自動運用ができるようになりました。

まとめ

今回はGoogle FormsとGASを使って、会議出欠確認のアプリ化とその自動運用の仕組みを作ってみました。

簡単な登録フォームならGoogle Formsで十分用が足りるな、という印象です。
そこにさらにGASを使って自動運用できる仕組みが加われば下手なソフトウェアやサービスを使うよりよっぽど便利かもしれないです。

昨今はローコード/ノーコードなシステム開発への需要が高まっています。
実際、そういった案件の話もよく聞きます。
Google Forms と GAS という強力な組み合わせは一度検討してみる価値があるかもしれません。

ご興味を持たれたら、ぜひ システム クラフト へご相談ください。
システム クラフトへの お問い合わせはこちらから。

お問い合わせ
システム クラフトへのご依頼など、お問い合わせはこちらからお願いします。 システム クラフトはシステムに関するお困りごとの解決をお手伝いします。

コメント

タイトルとURLをコピーしました