勤怠報告システムをGASを使って構築

チームメンバーの人数が増えてマネージャーひとりでは勤怠管理が難しくなり、Google製のツールと外部サービスを組み合わせて勤怠報告システムを作った時の話をします。
勤怠と書きましたが、遅刻・欠勤などの「怠」の方が対象です。
日常的にSlack等のチャットツールでコミュニケーションをとっているチームでしたら参考になる部分もあるかもしれません。


# 導入以前の状況

各々が、Slackの勤怠チャンネルに「電車遅延で遅れます」「来月〇日に有給休暇使います」など報告する運用でした。
5～6人のチームなら問題なく回っていても、10人を超えたころから管理コストがバカにならなくなり、このままじゃマズイと思ったのが今回のシステムを構築したきっかけです。
お金を掛けず、報告の敷居と管理の手間を下げることが目標です。


# 設計フロー

1. Googleフォームに必要事項を入力
2. Googleカレンダーに自動で反映
3. Slackにも通知


## 勤怠報告用のGoogleフォームを作成

![01.png](https://qiita-image-store.s3.amazonaws.com/0/254377/a08e644b-5b4b-1693-69c9-69a891fd4b3c.png)

こんな感じで作成しました。
このフォームで送信された内容を、Apps Scriptから操作できるようにします。
Googleフォームのメニューからスクリプトエディタを立ち上げます。

今回はフォーム、カレンダー、Slackとの連携を実現するため、
あらかじめスクリプト自体に外部リソースへのアクセス許可を与えておきます。

```JavaScript
function addPermission () {
  FormApp.getActiveForm();
  CalendarApp.getDefaultCalendar();
  UrlFetchApp.fetch();
}
```

上の関数をスクリプトエディタ上で実行すると、許可を求めるポップアップが表示されると思います。

![03.png](https://qiita-image-store.s3.amazonaws.com/0/254377/db1d2690-7271-29ad-b32a-e8a2f12b2f98.png)

一度許可した後は、先ほどの関数は削除してしまって構いません。
引き続きフォームの入力内容を取得する処理を記述していきます。

```JavaScript
function onFormSubmit(e) {
  // フォームの入力内容を取得
  var responses = e.response.getItemResponses();

  // 入力内容を使いやすい形に加工
  var formValues = {};
  for (var i = 0; i < responses.length; i++) {
    var question = responses[i].getItem().getTitle();
    var answer = responses[i].getResponse();
    formValues[question] = answer;
  }

  console.log(formValues);
}
```

次に、現在のプロジェクトのトリガーでフォーム送信時に `onFormSubmit` が実行されるように設定します。
設定後フォームを送信すると次のような値が取れると思います。

```JavaScript
{
  "名前": "山田 太郎",
  "日付": "2018-12-19",
  "勤怠種別": "遅刻",
  "理由": "電車遅延"
}
```

※ログはStackdriver Loggingに記録されます。⇒ [GAS でStackdriver Loggingを使う](https://qiita.com/HeRo/items/946dff8c9161a730ec66)


## 入力内容をGoogleカレンダーに自動反映

続いて、取得したデータをGoogleカレンダーに反映させます。
事前に新規カレンダーを作成し、設定ページからカレンダーIDを確認しておきます。

```JavaScript:onFormSubmitに追記
var calendar = CalendarApp.getCalendarById('xxxxx@group.calendar.google.com'); // カレンダーIDを設定
var event = calendar.createAllDayEvent(
  formValues['勤怠種別'] + '：' + formValues['名前'],
  new Date(formValues['日付']), 
  { description: formValues['理由'] }
);
```

フォームを送信してみましょう。

![02.png](https://qiita-image-store.s3.amazonaws.com/0/254377/ccd2fbd3-ab09-dde7-4b90-8bd71b0598d3.png)

反映されました。


## Slackにも内容を通知

次はフォームの送信内容をSlackでも通知するようにします。

Slackのアプリ一覧から **Incoming Webhook** を追加します。
投稿するチャンネルは適当なものを選択してください。
追加後に表示される画面で、**Webhook URL** を控えておきます。

```JavaScript:onFormSubmitに追記
var postUrl = 'https://hooks.slack.com/services/****/****/********'; // Webhook URLを設定する
var strDate = Utilities.formatDate(new Date(formValues['日付']), 'JST', 'MM月dd日');
var payload = JSON.stringify({
  text: strDate + 'は' + formValues['理由'] + 'のため' + formValues['勤怠種別'] + 'します！',
  username: formValues['名前']
});
return UrlFetchApp.fetch(postUrl, {
  method: 'post',
  contentType: 'application/json',
  payload: payload
});
```

Webhook URLに対してJSON文字列をPOSTします。
なお、[Utilities.formatDate()](https://developers.google.com/apps-script/reference/utilities/utilities#formatdatedate-timezone-format) は日付のフォーマットを行うApps Scriptの関数です。

![Slack](https://qiita-image-store.s3.amazonaws.com/0/254377/b1e60d09-490c-afa9-f881-d5c89cf4dfe9.png)

フォーム送信でSlack通知ができるようになりました。
当日の場合は日付を省略したり、理由に合わせてアイコンを変更してみたり、アイデア次第でいろいろ工夫できると思います。

# 報告者の手間を減らす

あとはメンバーにフォームのURLを周知すれば目的は達成、と言いたいところですが、報告する側の視点だと、毎回イチからフォーム入力するなら最初からチャットした方が楽かもしれません。

そこで、「名前」と「日付」が入力済み状態のフォームを作成してみることにします。


## 事前入力済みのフォームURLを発行

![事前入力したURLを取得](https://qiita-image-store.s3.amazonaws.com/0/254377/5b6321d9-60a8-9b91-7a4a-a17ea08f1a09.png)

「事前入力したURLを取得」というメニューがあるので、こちらを使用します。
名前と日付を入力し「リンクを取得」ボタンをクリックすると、URLがコピーできるようになります。

![事前入力したURLコピー](https://qiita-image-store.s3.amazonaws.com/0/254377/3361a08a-3f05-2aa2-1e4c-8ffe8c18fef3.png)

URLの形式は次のようになっています。

```
https://docs.google.com/forms/d/e/******/viewform?usp=pp_url&entry.00000000=【名前】&entry.11111111=【日付】
```

entry.00000000やentry.11111111といったクエリパラメータを動的に組み立てれば、入力者ごとにカスタマイズされたフォームを表示することができるはずです。
せっかくSlackと連携しているので、Slackのユーザーデータを利用してみましょう。


## Slackからフォームを呼び出す

ここまではGoogleフォームを起点に処理が走っていましたが、Slackのユーザーデータを使用するため、次のフローが先頭に加わります。

1. Slackでフォーム呼び出しのコマンドを入力
2. GASでリクエストを受け取り、user_idをもとにユーザーデータを取得
3. ユーザー名と本日の日付をクエリに含めたフォームURLを生成し、DMで通知

1 と 3 は [Slash Commands](https://api.slack.com/slash-commands)、2 は [Slack Web API](https://api.slack.com/web) で実現可能です。
順番に実装していきます。


### GASでPOSTリクエストを受け取る準備

```JavaScript
function doPost(e) {
  var user_id = e.parameter.user_id;
  console.log(user_id);
}
```

スクリプトエディタで上の関数を定義して、ウェブアプリケーションとして導入を実行すると、POSTリクエストを受けるためのURLが取得できます。
今後 `doPost()` の処理を変更するたび、プロジェクトバージョンを新しくする必要があることに注意が必要です。


### Slash Commands を作成

[Slack API: Applications](https://api.slack.com/apps) から登録します。今回は `/kintai` としました。Request URL には先ほど取得した「ウェブアプリケーションのURL」を入力します。
実際にコマンドを実行すると、SlackユーザーIDが取れるようになっていると思います。
[リクエストパラメータ](https://api.slack.com/slash-commands#app_command_handling)に含まれている`user_name`の値はSlackの表示名とは別物なので、そのままでは使えません。
そのため、Slack APIを経由してユーザー情報（表示名）を取得することとします。

### 3秒ルールについて

Slash Commandsには3秒以内にレスポンスを返さないとタイムアウトでエラーとなってしまう特徴があります。
GASはもともとのレスポンスが遅いことに加え、上記のAPIリクエストを加えると容易に3秒を超えてしまうため、いったんダミーのテキストを送って対応します。

```JavaScript
function doPost(e) {
  // いったんダミーレスポンスを返す
  UrlFetchApp.fetch(e.parameter.response_url, {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify({
      text: '次のURLから投稿してください！'
    })
  });
  // レスポンス本体
  return ContentService.createTextOutput(JSON.stringify({
    text: '【ここにフォームのURLが入ります！】'
  })).setMimeType(ContentService.MimeType.JSON);
}
```

### Slack API でユーザーデータを取得

先ほど作成した Slash Commands の App に対し **OAuth & Permissions** より `users.profile:read` の権限を付与したのち、Re:install して、OAuth Access Token を取得します。
最終的な doPost() は次のようなコードとなりました。

```JavaScript
function doPost(e) {
  // いったんダミーレスポンスを返す
  UrlFetchApp.fetch(e.parameter.response_url, {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify({
      text: '次のURLから投稿してください！'
    })
  });

  // ユーザー情報を取得
  var access_token = 'xoxp-0000000000-0000000000-0000000000-00000000000000000000'; // OAuth Access Token
  var getUrl = 'https://slack.com/api/users.profile.get?token=' + access_token + '&user=' + e.parameter.user_id;
  var res = UrlFetchApp.fetch(getUrl).getContentText();
  res = JSON.parse(res);
  var displayname = res.profile.display_name;

  // 本日の日付 YYYY-MM-DD
  var today = new Date();
  today = today.getFullYear() + '-' + (today.getMonth() + 1) + '-' + ('0' + today.getDate()).slice(-2);

  // レスポンス本体
  return ContentService.createTextOutput(JSON.stringify({
    text: FormApp.getActiveForm().getPublishedUrl()
      + '?usp=pp_url&entry.00000000=' + encodeURIComponent(displayname)
      + '&entry.11111111=' + today
  })).setMimeType(ContentService.MimeType.JSON);
}
```

これでSlackに `/kintai` と入力すると、ユーザー名と本日の日付が入力済みのフォームURLがDMで届くようになりました。


# さいごに

連携させるサービスを増やすと権限の管理が面倒になりますね。アクセストークン等はハードコーディングせずに、[Properties Service](https://developers.google.com/apps-script/reference/properties/) を使用すると一箇所で管理できて便利になると思います。
さて、このシステムを導入した結果ですが、報告の敷居が下がったせいか、弊社では欠勤・遅刻の回数が増えてしまう結果に…。ハードルを下げすぎるのも良くないようです。