GASで出社申請・承認システムを作る

# 概要
新型コロナウイルスの影響でリモートワーク推奨となった弊社、業務上フルリモートが厳しい社員もいるぞ〜
というわけで、出社する社員とそれにかかる交通費などの把握のため、出社は申請制にすることに。
その申請・承認のための簡易的なシステムをGASを使って作ることになりました。

# 使用ツール
- Googleフォーム
- Googleスプレッドシート（フォームの回答シート）
- Googleカレンダー
- Chatwork（弊社の社内コミュニケーションはこれ）

# システム要件(?)
要件は以下のような感じ。

- 出社申請した人が分かる
- 出社希望日時と理由（目的）が分かる
- 申請が労務担当に通知される
- 労務担当が申請を承認したらカレンダーに登録される
- 労務担当が申請を却下した場合はカレンダーの登録はなし
- 承認結果が通知される

これをもとに、システムの流れをこんな感じに設計。
![申請フォーム流れ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/471129/eb96dc46-b6fa-2aa2-7840-cefe03ee1185.png)

1. 出社したい社員がGoogleフォームに必要事項を入力し、申請する
2. Chatworkの専用グループにて、労務担当宛のタスクとして申請通知が届く
3. Googleスプレッドシートの申請内容を確認し、あらかじめ決めてある列に「承認」と入力する
4. 一定時間ごとに設定しているGASのトリガーが起動し、承認された出社申請をカレンダーに自動登録
5. 一定時間ごとに設定しているGASのトリガーが起動し、Chatworkの専用グループにて、結果（承認or却下）の通知が届く

それぞれの処理をするGASを書いていきます。

# 申請を労務担当に通知するGAS
```javascript:コード.gs
function submitForm(e){
  var itemResponses = e.response.getItemResponses();
  
  //申請内容の取得
  var name = itemResponses[0].getResponse(); //申請者名
  
  //通知本文の設定
  var message = name + ' さんから出社希望申請があります\n'
  + '詳細はシートを確認してください\n'
  + 'https://docs.google.com/spreadsheets/d/...'; //フォームの回答を記録したスプレッドシートのURL
  
  //Chatworkに通知送信
  sendToChatwork(message);
}

function sendToChatwork(body) {
  Logger.log('chatwork\n' + body);
 
  var token = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'; //チャットワークAPIトークン
  var roomId = 'XXXXXXXXX'; //通知を送信したいチャットグループのルームID
  
  var msg =  body ;
  var params = {
    headers : {"X-ChatWorkToken" : token}, //APIトークン
    method : "post",
    payload : {
      body : msg, //タスクの内容＝通知本文
      to_ids : 'XXXXXXX' //労務担当のアカウントID
    }
  };
  var url = "https://api.chatwork.com/v2/rooms/" + roomId + "/tasks";
  UrlFetchApp.fetch(url, params); //チャットワークAPIにリクエスト
}
```
申請内容を取得して通知本文を用意する関数（submitForm）と、Chatworkに送信する関数（sendToChatwork）を分けて書きました。
弊社の通知系GASはだいたいこうなってる。ほぼコピペ

こんな感じで労務担当にタスクが入ります。
![スクリーンショット 2020-09-16 18.32.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/471129/6b9b9bd7-8686-ee49-1cb7-a9f8dfffd520.png)


# カレンダーへの登録とChatworkへの通知を一緒にやるGAS
フォームの回答が記録されるスプレッドシートはこのような構成にしてあります。
![スクリーンショット 2020-09-16 18.06.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/471129/9920f484-20c5-f169-3fea-2929452d74ca.png)

- A~M列：フォームの回答記録欄（自動で入るところ）
- N~P列：労務担当が記録する欄
    - N：承認or却下
    - O：備考（却下の場合はその理由を記録したり、他に特筆すべきことがあれば記入する）
    - P：承認者（弊社は基本的には労務担当、場合によっては上長の場合も）
- Q~R列：GASで勝手に記録する欄
    - Q：カレンダーに登録した場合（承認された場合）、そのイベントID
    - R：チャットワークへの通知の有無

Q,R列によって、過去の申請が重複してカレンダーに登録されたり、通知されたりするのを防いでます。

GASの流れは、

1. A~R列すべてを配列で取得（2次元配列）
2. カレンダーの登録がまだない申請（=Q列が空欄の行）を探す
3. 承認されていればカレンダーに登録し、Chatworkへ通知
4. イベントIDと通知済みを該当配列に挿入
5. 2~4の処理をデータの数だけfor文でぶん回す
6. シートのデータを上書きする

という感じです。
もっと効率のいい方法がありそうですが浮かばなかったのでこうなりました。

```javascript:addEventsToCalendar.gs
function addEventsToCalendar() {
  var cal = CalendarApp.getCalendarById('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX@group.calendar.google.com'); //カレンダーIDでカレンダー取得
  var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('出社希望申請一覧'); //シートを取得
  var data = ss.getDataRange().getValues(); //シートデータを取得

  for(var i=2;i<data.length;i++){
    var evtDate = new Date(data[i][2]); //出社日
    var evtStartTime = new Date(data[i][3]); //出社時間
    var evtEndTime = new Date(data[i][4]); //退社時間
          
    var evtDateStr = (evtDate.getMonth()+1).toString().padStart(2,'0') + '月' + evtDate.getDate().toString().padStart(2,'0') + '日'; //出社日を文字列に変更し、二桁表示にする
    var evtStartTimeStr = evtStartTime.getHours().toString().padStart(2,'0') + ':' + evtStartTime.getMinutes().toString().padStart(2,'0'); //出社時間を文字列に変更し、二桁表示にする
    var evtEndTimeStr = evtEndTime.getHours().toString().padStart(2,'0') + ':' + evtEndTime.getMinutes().toString().padStart(2,'0'); //退社時間を文字列に変更し、二桁表示にする
    
    if(data[i][16] == ""){
      switch (data[i][13]){
        case '承認':
          var evtName = '【出社】' + data[i][1] + ' ' + evtStartTimeStr + '-' + evtEndTimeStr; //イベント名
          console.log(evtName);
          var myEvt = cal.createAllDayEvent(evtName,evtDate); //イベントの追加
          
          var msg = data[i][1] + ' さんの以下の申請が ' + data[i][13] + ' されました\n\n'
          + '出社希望日 : ' + evtDateStr + '\n' 
          + '出社希望時間 : ' + evtStartTimeStr + '~' + evtEndTimeStr + '\n\n'
          + 'インビジョンALLカレンダーに予定が入っていることを確認してください';
          sendToChatwork(msg); //Chatworkへの通知
          
          //シートへの入力
          data[i][16] = myEvt.getId();
          data[i][17] = '済';
          break;
          
        case '却下':
          var msg = data[i][1] + ' さんの以下の申請が ' + data[i][13] + ' されました\n\n'
          + '出社希望日 : ' + evtDateStr + '\n' 
          + '出社希望時間 : ' + evtStartTimeStr + '~' + evtEndTimeStr + '\n\n'
          + '却下の理由については上長に確認してください';
          sendToChatwork(msg); //Chatworkへの通知
          
          //シートへの入力
          data[i][16] = '-';
          data[i][17] = '済';
          break;
          
        default:
          break;
      }
    }
  }
  ss.getRange(1,1,i,18).setValues(data);
}

function sendToChatwork(body) {
  var CW_TOKEN = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'; // チャットワークAPIトークン
  
  var roomId = 'XXXXXXXXX'; //通知を送るチャットグループのルームID
  
  var msg = '[info][title]出社希望申請　承認結果のお知らせ[/title]' + body + '[/info]';
  
  var client = ChatWorkClient.factory({token: CW_TOKEN});
  client.sendMessage({
    room_id: roomId, 
    body: msg
  });  
}
```

承認されたらこんな通知が来ます。
![スクリーンショット 2020-09-16 18.32.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/471129/889c4cc3-1ecd-5010-0a3c-ee76afc966b1.png)

# 終わり
もうちょっとスマートな方法ないんかなぁ