毎日の健康調査をする仕組みを BOT で作ってみたよ

社員やメンバーの毎日の健康調査を記録できる仕組みを BOT で開発してみました！ヾ(*´∀｀*)ﾉ

経緯としてはね、こんなにもコロコロちゃんが流行したのでね。

「今朝の体温は何度だ？」「咳とか鼻水は出てないか？」( ﾟДﾟ)

と、リーダーのみなさんがメンバーに聞いているわけですよ。
んで、それを Excel にカチカチ記録して、一覧表にして人事部とかに「うちのチームはみな健康です！」って出したりするわけですよ。

た　い　へ　ん　！( ﾟДﾟ)

ってなわけで、BOT に任せちゃおうよ！って思ったわけです。

# どんな仕組み？

- LINEWORKS BOT に話しかけると健康調査が始まる
- 答えた内容は GoogleSpreadSheet に自動で記録されていく

簡単！(*‘∀‘)

#### 利用する技術

- Google Apps Script (GAS)
- Google SpreadSheet 
- LINEWORKS API

全部、**無料の範囲内でできるよ！**ヾ(*´∀｀*)ﾉ 最高かよ

GAS や LINEWORKS BOT の登録方法がわからないよ！って方は、下記記事で紹介していますのでよろしければ読んでくださいませませ。
[GoogleAppsScript で LINEWORKS のチャット BOT を作る](https://qiita.com/kunihiros/items/a94221ad7c9f4de84cf8)

# 実際の画面はこんな感じ

<img width="360" alt="ss1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363367/3a663dc1-6bef-3700-5826-fd37a6d168cd.jpeg">

<img width="360" alt="ss2" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363367/2d54ec5c-d4eb-20eb-139d-7d57522279ba.png">

<img width="360" alt="ss3" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363367/907bf507-70e7-5284-e20d-22e1df954ac0.png">

<img width="360" alt="ss4" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363367/0b9a67a9-bffc-24e5-1648-b6f299e779e6.png">

<img width="360" alt="ss5" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363367/db15a7ca-a901-15e7-1083-e211010c4713.png">

<img width="360" alt="ss6" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363367/ebc0495a-5901-7698-c71a-2cd511a11be9.png">

<img width="360" alt="ss7" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363367/6918fe07-98b0-ba3a-a4e2-c1f51d53b3ea.png">

<img width="480" alt="ss8" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363367/b1514717-d97f-e49b-4e7b-9249bcf1324e.png">

私、美術センスが壊滅的なので、デザイン的なものには突っ込まないでね！( ﾉД`)ｼｸｼｸ…

アカウントのところはモザイクかけちゃっていますが、もちろん誰の記録かわかるようになっています！
なので、この SpreadSheet を見ればみなの健康状態が一目瞭然なのですヨ！(*‘∀‘)

# コードの公開

それでは、コードをズラズラっと。

```js:app.gs
function setOptions(){
  return {
    "apiId" : "xxxxxxxx",
    "consumerKey" : "xxxxxxxxxxxxxxxx",
    "serverId" : "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "privateKey" : "-----BEGIN PRIVATE KEY-----\nxxxxxxxxxxxxxxxxxxxxxxxxx\n-----END PRIVATE KEY-----",
    "botNo" : 000000000
  };
}

function doPost(e) {
  if (e == null || e.postData == null || e.postData.contents == null) return;
  const requestJSON = e.postData.contents;
  const requestObj = JSON.parse(requestJSON);
  if (requestObj.content.text == "利用開始") return;
  const accountId = requestObj.source.accountId;
  
  // postback 対応
  const postback = requestObj.content.postback;
  var templateNo = 1;

  // postback message 受信時に次の temlateNo の取得および spreadsheet の操作
  if(postback) { 
    const text = requestObj.content.text;
    // spreadsheet 取得
    const ss = SpreadsheetApp.getActive();
    var sheet = ss.getSheetByName(accountId);
    switch(postback){
      case "start" :
        if(text == "はい"){
          templateNo = 2;
          if(sheet == null) sheet = ss.insertSheet(accountId);
          var date = new Date();
          sheet.getRange("A1").setValue([Utilities.formatDate( date, 'Asia/Tokyo', 'yyyy/MM/dd')]);
        } else { templateNo = null }
        break;
      case "bodyTemperature_1" :
        templateNo = 3;
        sheet.getRange("B1").setValue(text);
        break;
      case "bodyTemperature_2" :
        templateNo = 4;
        sheet.getRange("C1").setValue(text);
        break;
      case "bodyTemperature_3" :
        (text == "はい") ? templateNo = 5 : templateNo = 2;
        break;
      case "condition" :
        templateNo = 6;
        sheet.getRange("D1").setValue(text);
        break;
      case "hospital" :
        templateNo = 7;
        sheet.getRange("E1").setValue(text);
        break;
      case "medicine" :
        templateNo = 8
        sheet.getRange("F1").setValue(text);
        break;
      case "recode" :
        if(text == "はい"){
          templateNo = null;
          // キャッシュを DB に統合
          const values = sheet.getRange("A1:F1").getValues();
          const data = [values[0][0], accountId, values[0][1] + values[0][2]/10, values[0][3], values[0][4], values[0][5]];
          ss.deleteSheet(sheet);
          sheet = ss.getSheetByName("シート1");
          sheet.appendRow(data);
        } else { templateNo = 2 };
        break;
      default : break;
    }
  }
  
  // LINEWORKS にメッセージを送信
  if(templateNo){
    sendButtonMsg(setOptions(), accountId, setButtonTemplate(templateNo, accountId));
  } else {
    LINEWORKS.sendMsg(setOptions(), accountId, "健康調査を終了します。\nお疲れ様でした。");
  }
  return
}
function setUp() {
  const obj = setOptions();
  const uri = "https://apis.worksmobile.com/r/" + obj.apiId + "/message/v1/bot/" + obj.botNo + "/persistentmenu";
  const json = {
    "content": {
        "actions": [{
            "type": "message",
            "label": "健康調査を開始する"
        }]
    }
  };
  const options = {
    "method": "post",
    "headers": LINEWORKS.setHeaders(obj),
    "payload": JSON.stringify(json)
  };
  UrlFetchApp.fetch(uri, options); 
}

function sendButtonMsg(obj, accountId, content){
  const uri = "https://apis.worksmobile.com/r/" + obj.apiId + "/message/v1/bot/" + obj.botNo + "/message/push";
  const json = {
    "accountId": accountId,
    "content": content
  };
  const options = {
    "method": "post",
    "headers": LINEWORKS.setHeaders(obj),
    "payload": JSON.stringify(json)
  };
  UrlFetchApp.fetch(uri, options);  
}

function setButtonTemplate(templateNo, accountId){
  var contentText;
  var actions = [];
  const yn = ["はい","いいえ"];
  var ss = SpreadsheetApp.getActive();
  var sheet = ss.getSheetByName(accountId);
  
  switch(templateNo){
    case 1 :
      contentText = "健康調査を開始しますか？";
      for(var i = 0; i < yn.length; i++){
        actions.push({"type": "message", "label": yn[i], "postback": "start"});
      }
      break;
    case 2 :
      contentText = "今日の体温を教えてください。\n何度代でしたか？";
      for(var i = 35; i <= 41; i++){
        actions.push({"type": "message", "label": String(i), "postback": "bodyTemperature_1"});
      }
      break;
    case 3 :
      contentText = "続けて細かい体温を教えてください。\n何分でしたか？";
      for(var i = 0; i <= 9; i++){
        actions.push({"type": "message", "label": String(i), "postback": "bodyTemperature_2"});
      }
      break;
    case 4 :
      const bodyTemperature = sheet.getRange("B1").getValue() + sheet.getRange("C1").getValue()/10;
      contentText = "今日の体温は " + bodyTemperature + "℃ですね？";
      for(var i = 0; i < yn.length; i++){
        actions.push({"type": "message", "label": yn[i], "postback": "bodyTemperature_3"});
      }
      break;
    case 5 :
      contentText = "以下のような自覚症状はありますか？";
      const condition = ["咳","のどの痛み","くしゃみ・鼻水","倦怠感（だるさ）","息苦しさ","特になし"];
      for(var i = 0; i < condition.length; i++){
        actions.push({"type": "message", "label": condition[i], "postback": "condition"});
      }
      break;
    case 6 :
      contentText = "医療機関を受診しましたか？";
      for(var i = 0; i < yn.length; i++){
        actions.push({"type": "message", "label": yn[i], "postback": "hospital"});
      }
      break;
    case 7 :
      contentText = "薬を服用しましたか？";
      for(var i = 0; i < yn.length; i++){
        actions.push({"type": "message", "label": yn[i], "postback": "medicine"});
      }
      break;
    case 8 :
      const temperature = sheet.getRange("B1").getValue() + sheet.getRange("C1").getValue()/10;
      const symptoms = sheet.getRange("D1").getValue();
      const hospital = sheet.getRange("E1").getValue();
      const medicine = sheet.getRange("F1").getValue();
      contentText = "（本日の健康状態）\n体温：" + temperature + "℃\n自覚症状：" + symptoms + "\n医療機関の受診：" + hospital + "\n薬の服用：" + medicine + "\n\n 以上でよろしいですか？";
      for(var i = 0; i < yn.length; i++){
        actions.push({"type": "message", "label": yn[i], "postback": "recode"});
      }
      break;
    default : break;
  }
  return {
    "type": "button_template",
    "contentText": contentText,
    "actions": actions
  };
}
```

もっと綺麗に書けるのだろうけど、取り敢えず動くことを優先させました。
あと、エラー回避してません。運用しながらちょびちょび修正かけてます。

### LINEWORKS ライブラリの利用

認証周りとか自前でやると面倒なので、LINEWORKS ライブラリを利用しています。
LINEWORKS ライブラリの登録方法は [こちら](https://qiita.com/kunihiros/items/a94221ad7c9f4de84cf8#3-%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%82%92%E7%99%BB%E9%8C%B2%E3%81%99%E3%82%8B) を参考にしてください。

### 苦労したところ

アカウントごとに記録する際に、セッション管理に悩みました。
結局、アカウントごとに一時的に Sheet を作成して、最後の大元の Sheet に転記して削除するようにしました。

### 今後、拡張したい機能

エラー処理はもちろん入れないとですよねー。今のままだと不安定ですし。
セッションが残っている場合に備えて定期的に削除したり。

37.5℃以上のメンバーがいたら管理者に通知するとか。
Weekly レポートを作成して送信するとか。

色々と要望を聞きつつ、増やしていきたいですね！

#おわりに
ここまでお付き合いいただきありがとうございました。

久々にサービスっぽいもの作った！楽しい！(*‘∀‘)
何かを作るのは本当に楽しいですね～♪

これ、みんなが利用できるように、ライブラリ作って初期設定を簡素化したら、使ってくれるかな？
コロコロブームが去ってもリモートワークを続けるところも多そうですし、メンバーの健康把握も課題の一つになってくるのでは・・・？

ちょっと頑張ってみようかな('ω')

ではまた！(^^)/

#参考にさせていただきましたm(_ _)m
[GoogleAppsScript 公式サイト](https://developers.google.com/apps-script/reference/)
[LINEWORKS Developers](https://developers.worksmobile.com/jp/document?lang=ja)