なしとげたいこと
会社主催イベントの参加受付作業をなるべく簡単にしたい!
気が付いたらイベントの担当者になっていたけど、5年間事務員やってきたからこそ自分が事務処理(特に対人的なやつ)不得意と痛いほど自覚しているので、受付事務的な作業は自動化をしたいと目論みました。
そこで応募側のメリットはご存知の通り入力の手軽さですが、受付側としては下記の利点からGoogleフォームを利用することにしました。
1. 回答受付のスプレッドシートで自動的に参加者リストが生成される
2. 入力必須項目の設定で必要な情報の漏れを防ぐことが出来る。
3. Slackと連携が容易
3番目の利点について、フォーム側の設定でメールの通知が可能な中Slackと連携する必要性は社内的なものです。招待メールの送信自体は営業さんから送ってもらうので、参加のお礼も営業担当からするわけですが、問題は営業さんが複数いること。自分の担当でない顧客からの回答までメールで通知が着たらうっとおしいですよね。
なので会社のSlackワークスペースに「#イベント参加通知」というチャンネルを作り、そちらに知らせが飛ぶようにしました。
処理をしたら絵文字を付けたりすると周りから進捗を気に掛けられるのでそこも良いですよね。
結果のところで後述しますが、企画中に募集の窓口を増やしたことによる混乱を起こさず、スムーズに運営出来たのも大きなメリットでした。
↓フローとしてはこんな感じで。ホントは管理表も1枚で出来るとスマートですが、種々の都合で今回は断念しました('_`)
では目的を遂げるために具体的に必要な機能を実装していきます。
Google Formの設定
参加人数に応じて質問を変えたい
今回のイベントは公募ではなく取引先担当者を招待する形で、かつ各社2名まで参加OKとしているので参加者個人からではなく先方担当から取りまとめての応募になります。
そのため同行者がいる場合のみ、その情報を入力する質問が表示されるようにしたいところ。
これはGoogle Form内の「セクション」という機能を使えば出来ますね。
やり方は公式ヘルプをどうぞ
https://support.google.com/docs/answer/141062?hl=ja
回答者に不必要な質問を答える手間をかけさせないと同時に、今回のような「回答内容次第では必須項目にしたいが、聞く必要自体ない人もいる質問」がある時にセクション機能が役立ちます。
同行者の情報が記入漏れしているとこっちが困りますが、
同行者がいないのに同行者の情報が必須項目だと向こうが困ってしまいますからね。
この記事の内容を実装したエントリーシートがこちらです。
↓↓↓
https://goo.gl/forms/uTzlQcDAzvnXCJwy2
ただセクションはこのようにページが分かれることで少しユーザビリティが下がるため、使う必要無ければ使わない方が良いように思います。
ちなみに回答すると私の個人Slackに飛びます^^個人情報は送らないでね
社外用フォームを作成する際に注意が必要な設定
歯車マークの設定欄で「回答を一回に制限する」という設定が出来ますが、上にも下にも注意書きがある通りGoogleアカウントが無いと回答出来なくなります。
なのでGsuiteを導入している会社内用とかでなければ基本設定してはいけません。
いや見たら分かるんですけど、別件で見落としてしまったことがあったので…
またメールアドレスを収集する設定ですが、設定すると入力必須のアドレス入力欄が挿入されるという、それだけの設定です。(何かと紐付けて、入力しなくても勝手に取得されるのかと期待した人)
フォームに入力された内容を取得してSlackに飛ばす
SlackとGASの連携については先達のまとめがあるのでそちらを参考に
●【初心者向け】GASを使ってSlackへ自動通知
● Googleフォームの回答をGoogle Apps ScriptでSlackに通知するための作業手順
↑下に書いたコードは大体こちらそのまま流用させてもらってます。
「登録フォームにチェックボックスを設置したい」
「GASによる回答の取得方法の仕様をもう少し詳しく知りたい」
という方以外は上記の記事を読めば事足りると思います。
実際のコードと結果
FormApp.getActiveForm();
var form = FormApp.getActiveForm();
// Formの送信内容を取得する
function onFormSubmit(e) {
var itemResponses = e.response.getItemResponses();
var singlePerson = itemResponses[6] === undefined;
if (singlePerson) {
company = itemResponses[0].getResponse();
name1 = itemResponses[1].getResponse();
adress1 = itemResponses[2].getResponse();
number = itemResponses[3].getResponse();
purpose = itemResponses[4].getResponse();
other = itemResponses[5].getResponse();
name2 = "無し";
adress2 = "無し";
} else {
company = itemResponses[0].getResponse();
name1 = itemResponses[1].getResponse();
adress1 = itemResponses[2].getResponse();
number = itemResponses[3].getResponse();
name2 = itemResponses[4].getResponse();
adress2 = itemResponses[5].getResponse();
purpose = itemResponses[6].getResponse();
other = itemResponses[7].getResponse();
}
sendToSlack( responseMessage(company, name1, adress1, number, name2, adress2, purpose, other) );
}
// slackにメッセージとして送信したいテキストを作成する
function responseMessage(company, name1, adress1, number, name2, adress2, purpose, other){
var message = "フォームから参加申し込みのエントリーがありました。\n\n";
message += "会社名:" + company +"\n";
message += "申込者氏名:" + name1 +"\n";
message += "申込者メールアドレス:" + adress1 +"\n";
message += "参加人数:" + number +"\n";
message += "同行者氏名:" + name2 +"\n";
message += "同行者メールアドレス:" + adress2 +"\n";
message += "参加の目的:" + purpose +"\n";
message += "その他要望など:" + other +"\n";
return message;
}
//Slackに送信
function sendToSlack( messages ){
var url = '****************************'; //取得したSlack Incoming Webhook URL
var channel = '#test'; // メッセージ投稿先のチャネル名
var botname = '参加申し込み通知Bot'; //BOTの名前
var icon_emoji = ':postbox:'; // Botのアイコンを指定
var jsonData =
{
"channel" : channel,
"username" : botname,
"text" : messages,
'icon_emoji' : icon_emoji
};
var payload = JSON.stringify(jsonData);
var options =
{
"method" : "post",
"contentType" : "application/json",
"payload" : payload
};
UrlFetchApp.fetch(url , options);
}
※指定できるiconは EMOJI CHEAT SHEETを参照しました。
スクリプトのトリガーを「フォーム送信時」にするのをお忘れなく!
フォームが送信されるとこんな感じで通知が来るはずです
(上が2名での申し込み、下が1名での申し込み)
GASで質問項目を取得する
まずフォーム送信をトリガーにしたonFormSubmit(e)
のe
に渡された送信内容から、
回答を取得する方法は2つあります。
e.namedValues["質問項目名"]
質問項目の名前から回答を取得できます。
これを使うとスクリプトが読みやすい?のと、質問の順番変更の影響を受けないのがメリット。
ただ私の場合、営業との打ち合わせなどで質問項目を微修正してスクリプトの修正を忘れるというミスが目に見えたので使わないことにしました。
e.getItemResponses()
回答の内容stringが配列で取得できます。
(タイムスタンプは含まれません。必要であればgetTimestamp()
を使いましょう)
後は質問の順番を見ながら.getResponse()
で中身を取り出すだけ。
質問の順番変更に弱いものの、質問項目の内容には影響を受けません。
解説記事を見ると大体こっちですね。
今回はこちらを使った場合の解説です。
飛ばしたセクションの質問の回答はどう処理される?
このフォームを例にすると1人での参加と登録した場合、セクション2は回答者に表示されない、つまり入力されることもありません。
では「1人」とした場合にgetItemResponses()で得られる配列はというと、
セクション2の内容は何も渡されず、セクション1 + セクション3の回答のみが入った配列が返されます。
なのでコードの方では、
セクション1と3の項目数(6個)を超える回答数がない場合をsinglePerson
として条件分岐を行い、
1人参加の場合は2人目の情報項目に「無し」という文字列が渡るようにしています。
↓この部分
var singlePerson = itemResponses[6] === undefined;
if (singlePerson) {
company = itemResponses[0].getResponse();
name1 = itemResponses[1].getResponse();
adress1 = itemResponses[2].getResponse();
number = itemResponses[3].getResponse();
purpose = itemResponses[4].getResponse();
other = itemResponses[5].getResponse();
name2 = "無し";
adress2 = "無し";
} else {
company = itemResponses[0].getResponse();
name1 = itemResponses[1].getResponse();
adress1 = itemResponses[2].getResponse();
number = itemResponses[3].getResponse();
name2 = itemResponses[4].getResponse();
adress2 = itemResponses[5].getResponse();
purpose = itemResponses[6].getResponse();
other = itemResponses[7].getResponse();
}
任意回答の質問の回答はどう処理される?
Google Formでは項目を必須回答にするか任意回答にするか設定できます。
必須にした場合はもちろん空欄では送れなくなり、最後の方の目的とか備考の欄ってぶっちゃけ必須だとちょっとうっとおしいので任意にしたかったのですが、ここに意外と深い穴がありました。ハマりました。
ハマった過程は良いとして、結論としては
「チェックボックスやプルダウン式の項目には入力必須を付けよう」
です。チェックボックスの場合は「その他」でテキスト入力できる機能も付いてますし、なんなら「回答なし」という選択肢を付ければ良いと思います。
任意質問の何が問題かというと、
「回答されなかった時に空の値が渡るタイプの項目と、回答の存在自体が配列に渡されないタイプの項目がある」
というところで、
例えば「最後から2番目の目的についての質問に未回答で、備考を入力された場合」に、
Slackの方の表示で目的の欄に備考の内容が表示される事態となります。
この謎の回答ズレ込み問題が分からなくて丸1日ほど泣いていたのですが今回初めて、公式リファレンスを読んだら普通に解決するというエンジニアあるある?経験が出来たのは良かったです。
getItemResponses()
Gets all item responses contained in a form response, in the same order that the items appear in the form. If the form response does not contain a response for a given TextItem, DateItem, TimeItem, or ParagraphTextItem, the ItemResponse returned for that item will have an empty string as the response. If the form response omits a response for any other item type, this method excludes that item from its returned array.フォームからの回答内容に含まれる回答たちをフォーム内と同じ順番ですべて取得します。
もしテキスト・日時・時間・パラグラフタイプの項目が未記入の場合、その項目は回答として空のstringを返します。またその他のタイプの項目の入力が省略された場合、その回答は結果の配列から除外されます。。ということ、多分
かなり逃げた解決方法で自分としても不甲斐ないのですが、
回答されていない質問があるべき配列番号にスクリプト側で任意の文字列を挿入するという処理で出来そうな気がしつつ、「答えられていない質問」を取得する方法が見つからなかったのと時間なかったので入力側の制限に逃げましたε=ε=ε=ε=ε=┏( ・_・)┛
※今思いついたけど、フォームの項目名=Slackの項目名の場合は、
getTitle()で回答に対する項目名を取得して、
Slack投稿文の生成で項目の方も変数にしてしまえば行けるかも?(試してない)
でも社外向けのアンケの項目名って基本長くなるやん…説明文使うにしても。
結果:かなり良かったです
目的だった営業さんにも分かりやすいと言っていただけ、偉い人にも上手いやり方だったとお褒めの言葉をもらえました。
イベント自体も一応上手くいったみたいで、それも自分的に受付作業という地味に時間喰う作業を省力化して他に注力できたのは結構大きいと感じます。ただもう2度とはやりたくないですが…
それと最初のところで言った企画中に募集の窓口を増やしたことによる混乱を起こさず、スムーズに運営出来たという後から分かったメリットをまとめて終わります。
社内での情報共有・相談のしやすさ
要するに最初、取引先などに声をかけていたもののまぁー集まらない…
という悲しい事態を受けて一般募集に踏み切ったことで起きた問題、それに対してGoogleフォームとSlackの連携が効果的でした。
具体的には下記のような点です。
誰の営業担当でもない人から応募が来る。
今までは参加登録した人にはその営業担当から返事をすれば良かったのですが、一般参加だと当然全く接点のない方から応募が来ます。
自分の担当じゃないしと誰も返事をしない事態を避けるため、その都度そのSlackチャンネルで分配がスムーズに出来ました。
一般参加の参加者でも背景が共有できる
どこから聞き付けてこんな方が!?という方から応募が来た時に、
社長がチャンネルを見て「あ、その人知り合い」と教えてくれたりします。
その他参加者への情報交換はそのチャンネルに書くだけなのでスムーズですし、好きな時に見返せます。
置きチラシからの参加も一元化できる
機能したかは謎ですが、もう近くのコワーキングスペースにチラシ置かしてもらおうや!という話になった時、チラシにフォームへのQRコードを載せるだけだったので対応自体は凄く楽でした。
応募の窓口は増やしても、受付方法は増やさないのが対応の抜け漏れを減らすためには大事ですよね。