NotionとGASを使ってタスク管理継続するために足掻いた話

# はじめに
初記事の投稿になります。
M1にもなって初記事なんてこれまでサボってきていたことが盛大にバレていますが、恥を忍んで胸を張って書いていきます。
この記事はMYJLab Advent Calender2022の16日目の記事になります。
前回は@hcpmiyukiの[【SlackBot】冬の登校時間を管理するBotを作った🐱](https://qiita.com/hcpmiyuki/items/4696d22aacf609c6095d#%E4%BB%8A%E5%9B%9E%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8Bslack-api%E3%81%AEmethod)でしたが、今日も似たような記事になってしまって恐縮です、、、。

## 書かれていること
タスク管理の環境だけ整えて満足してしまう現象あるあるですよね。そこで、botさんの力を借りてタスクがちゃんと遂行できることに満足できる人間になれるように頑張った話です。
ざっくりと項目としては以下です。
* タスク管理継続しないあるあると解決の方向性
* Notionの構成と拡張機能の紹介(テンプレート)
* Slackで期限の通知
* Slackからタスクの状況を操作
* GASのコード

## 書かれていないこと
* Notion APIの詳細な説明
* Slack ワークフロービルダーの詳細な説明
* Slack Webhookの詳細な説明
* GASの定期実行とかの詳細な説明

つまり、詳細な説明書く余裕なかったので参考文献見てください、、、🙇

# 参考文献
一応最初の取っ掛かりになったのはこの記事です。
普段Notion使ってるので、それ版作ろーと言うのと、もうちょい僕が普段使えるようにしたかったのがきっかけになりました。
https://qiita.com/t-ninomiya/items/8dc5beb71859cea747b2

その他色々参考にしましたので以下に参照したもの列挙しておきます。
* https://developers.notion.com/reference
* https://tektektech.com/notion-api/
* https://qiita.com/chihiro/items/c7b11abc78f5d806c3a8
* https://www.bizzcode.net/%E3%80%90gas%E3%80%91webapi%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%80%81%E7%89%B9%E5%AE%9A%E3%81%AE%E3%82%B9%E3%82%BF%E3%83%B3%E3%83%97%E3%81%8C%E6%8A%BC%E3%81%95%E3%82%8C%E3%81%9Fslack%E3%81%AE/
* https://qiita.com/zoe7896/items/3082a31e00d9db3df146
* https://auto-worker.com/blog/?p=6383
* https://auto-worker.com/blog/?p=4490#toc_id_3_2


# タスク管理継続できないマンを理解する
タスク管理して満足する人というのは、以下のような人のことを言います。
> **タスク管理継続できないマン**
タスクの管理頑張ろうとめっちゃ気合い入れるが、継続せずタスクがなかなか遂行されない人。またその様。
夏休みの課題は最初に計画立てるが、最終的に一夜漬けになっていたタイプに多い。  (個人的見解)

まったくもって僕はこれに当てはまっているわけですが、もういい大人ですしいい加減卒業しないといけないと思っております。そんなわけで今年に入ってタスク管理継続できないマンからの卒業に向けて足掻いた記録を残していこうと思います。
また同じような人に参考になってもらえると嬉しいですが、特に以下みたいな人により参考になってほしいなと思います。
* Slackを開かない日がない
* タスク管理Notion使ってる(使おうとしてる)

## タスク管理して満足してしまう原因
この現象には長年悩まされていますので、「これ良さそう！」とアイディアベースで構築していくのは良くないことを知っています。そのため、まずは管理だけして満足してしまう原因から解決策を考えたいと思います。

### 原因①：タスクの追加がめんどくさい
せっかくいい感じで管理の環境を作っても、色々入力する要素が多くて大変なんですよね。
タスクの期限、タスクのカテゴリ、参照記事のURL、、、。
なのでタスクあるけど追加しなくなって、「Slackにメモしときゃいいか」とか言って結局環境を使わなくなるみたいな、、、。


### 原因②：タスク管理ツールを開く習慣がなくて操作しなくなる
タスク管理って毎日触らないと意味ないんですよね。
今回の場合だとNotionでタスク管理してても、普段使ってないと「昨日のタスクの入力してないわ、、、」みたいになります。それが増えると結局タスク管理のためのタスクが増えて、、、という悪循環になります。

### 原因③：タスク期限を守れなくて萎えて使わなくなる
人間決めた期限通りに動ける人のほうが少ないと思います。
期限は守ったほうがいいですが、期限過ぎてしまったくらいでタスク管理しなくなってタスク状況がカオスになる方が良くないんじゃないでしょうか。ですので期限の延長に関しては寛容に、システム的には手軽に期限が延長できるようにしたほうがいいと思います。
(あくまで期限守れない原因が、タスク管理の使い方にあるという要因に関してで怠惰的な部分には他のアプローチが必要ですよねきっと、、、)


## 改善案考えてみた
そんなわけでどうやっていこうかって話なんですが、以下のようにまとめてみました。
基本的に普段触る機会の多いSlack使ってうまくやりたいって感じなので、それが伝わるとありがたいです。
| タスク管理続かない原因 | 改善案 |
|--------------------|-------|
| 原因①：タスクの追加がめんどくさい | -WEBページから直でタスク追加できるようにする<br> -Slackからタスク追加できるようにする |
| 原因②：タスク管理ツールを開く習慣がなくて操作しなくなる | -Slackに通知するようにする |
| 原因③：タスク期限を守れなくて萎えて使わなくなる | -Slackからタスク期限伸ばせるようにする |

これだけでは、タスク管理続かないやつはこれだけじゃ改善しないって声がもちろん聞こえます。
僕も試行錯誤中なのでいい感じになったらまた記事書くので、今回は許してください。
以下でそれぞれについて書いていきます。


# ここから本編
## Notionの構成
Notionの構成自体に結構こだわってはいまして、紹介したい部分が結構あるんです、、、。
ただ、この記事の本筋からズレちゃうので、僕が使っているのとほぼ同じ構成のテンプレートおいておきますので参考にしてみてください。こだわってる部分の話はまたどこかで書くかも。

[Notion テンプレート](https://innate-bee-5a0.notion.site/2022-ead4bd90efc24e6b92b3c3032dd6f8ab "2022アドベントカレンダー用テンプレート")
![Screen Shot 2022-12-14 at 23.34.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/666097/268b0d11-b252-139b-7da1-617bbe761988.png)


## WEBページから直でタスク追加できるようにする
これは既存のChrome拡張機能使います。
Notion Web Cliperというやつです。
https://chrome.google.com/webstore/detail/notion-web-clipper/knheggckgoiihginacbkhaalnibhilkk?hl=ja

これ使うとWEBのページの内容が以下の紹介のようにNotionに格納してくれます。
https://qiita.com/shushutochako/items/0865799ec18c7ef90996#notion-web-cliper-chrome-extension

ざっくりと説明すると、
**これをこうしたら**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/666097/3acf49ac-e480-4e6b-319f-21d4fd0a74e0.png)

**ほら入った天才。**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/666097/1dbe8a6c-c83b-783f-6557-c521bdfc2c6b.png)

これ使う理由は **この記事、後で試してみよーとかそう思ったときにわざわざNotion開いて・・・** がめんどくさいからですね。
なのでこれでWEBページごとタスクリストに格納してしまう感じですね。(期限とかは後で開いたときに設定する)
読みたい論文が出てきたら、これ使うとかが僕が使う用途としては多いです。あとは後で読んでおきたい記事とかもこれ使ってストックしてあります。色々他にも拡張機能出てますので、機能違ったりするので見てみるといいと思います。
ただ、かゆいところ手が届いてない印象もあるので今度自分でも作ろうかなとも考えてます。(公式さん頑張って、、、)

## Slackからタスク追加できるようにする
ここも、タスク追加するのがめんどくさい事改善するために工夫した話です。
普段基本的にはSlack上で生きてるので、Slack上でタスクが発生することよくあります。
このときにまたNotion開いて・・・がめんどくさいわけです。
これの実装もして使ってたんですが、昨日公式さんの方からリリースがありました。
実装してた機能は以下に出てくる「Slackからタスク期間伸ばせるようにする」機能とほぼ同じなので、リリースの紹介だけしておきます。

https://twitter.com/NotionHQ/status/1603129150318948352?s=20&t=BJDq-cngqhpsCO_YPySkzQ

つまりSlackのメッセージをNotionに追加できる機能ですね。
いちいちNotion開かなくていいのですごくありがたい話です。(期限やページのカテゴリみたいなプロパティもここから入れられるみたいなのでさすがわかってるな公式さんって感じ)

## Slackに期限の通知するようにする
タスクに設定している遂行期間に今日が当てはまっていたら通知するってものです。
Slackに通知するんですが以下みたいな感じで送られてきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/666097/fe4344a5-96a9-682b-08b5-a42e34bee7d1.png)
期限が迫っているタスクがありますねぇ、、、がんばりますw
一応GASのコードも貼っておきます。解説する余裕ないのでご了承ください(参考文献とか見てください)
```notification_to_slack.js
function todayTaskNotification(){
  const result = todayTasksFetchURL();
  const properties = setTaskProperty(result);
  notificationToSlack(properties);
}

function setUserProperty() {
  const user_properties = PropertiesService.getUserProperties();

  user_properties.setProperties({
    'NOTION_KEY': '******************************',
    'NOTION_DATABASE_ID': '******************************',
    'SLACK_WEBHOOK_PARAMETER': '******************************'
  });
  return user_properties
}

function notificationToSlack(properties){
  user_properties = setUserProperty()
  const request_url = "https://hooks.slack.com/services/" + user_properties.getProperty("SLACK_WEBHOOK_PARAMETER");
  const headers = {
    "Accept" : "application/json",
  };
  var content = ""
  Logger.log(properties)
  if (properties.length>0){
    for (i=0; i<properties.length;i++){
      content = content
              + "■" + String(properties[i][1]) + "\n"
              + String(properties[i][0]) + "\n" 
              + "進捗: *" + String(properties[i][2]) + "*\n"
              + "タスク期限: " + String(properties[i][3]) + "\n"
              + "期限まで残り" + String(properties[i][4]) + "日です！\n"
              + "-----------------------------------------" + "\n"
    }
  } else {
    content = content + "溜まっているタスクはないよ！天才！"
  }

  const payload = {
        'text': "<@UFF1BR98A>",
        'attachments': [
            {
                'color':'danger', 
                //  プリセットでは[good, warning, danger]の3種類があるが、カラーコードも利用可
                'fields': [
                    {
                    'title': "*こちらのタスクを今日やることになっているよ*", 
                    'value':  content
                    }
                ]   
            }
        ]
  }

  const request_options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(payload)
  }
  
  // Logger.log(payload);
  UrlFetchApp.fetch(request_url, request_options);
}

function todayTasksFetchURL() {
  user_properties = setUserProperty()
  const request_url = "https://api.notion.com/v1/databases/" + user_properties.getProperty("NOTION_DATABASE_ID") + "/query";
  // Logger.log(requestUrl);

  const headers = {
    "Content-Type" : "application/json",
    "Notion-Version" : "2022-06-28",
    "Authorization" : "Bearer " + user_properties.getProperty("NOTION_KEY"),
  };

    // remindプロパティのチェックが付いてないものを対象
  const payload = {
    "filter" : {
      "and" : [
        {
          "property" : "進捗",
          "select" : {
            "does_not_equal" : "done"
          }
        }
        ,{
          "property" : "ステータス",
          "select" : {
            "does_not_equal" : "アーカイブ"
          }
        }
      ]
    }
  };

  const request_options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(payload),
  }

    // APIに通信
  const response = UrlFetchApp.fetch(request_url, request_options);

  // resultsキーにページ情報のオブジェクトの配列が格納されている
  const result = JSON.parse(response.getContentText()).results;
  // Logger.log(result);
  return result;
}

function setTaskProperty(notion_results) {
  let results = [];

  for(var i=0; i<notion_results.length; i++){
    const id = notion_results[i].url;
    const name = notion_results[i].properties["Name"]["title"][0].plain_text;
    const start_date = notion_results[i].properties["start_date"].formula.date['start'];
    let progress = null
    let end_date = null

    if( notion_results[i].properties["進捗"].select != null){
      progress = notion_results[i].properties["進捗"].select.name;
    }

    if(notion_results[i].properties["end_date"].date != null){
      end_date = notion_results[i].properties["end_date"].date['start'];
    }


    // 日付のフォーマット揃える
    const date_results = formatDate(start_date,end_date);

    if (date_results[1] <= date_results[0] && date_results[0] <= date_results[2]){
      results.push([id,name,progress,date_results[2],date_results[3]]);
    }
  }
  Logger.log(results);
  return results;
}

function formatDate(start_date,end_date){
  const today = new Date();
  const new_start_date = new Date(start_date);
  const new_end_date = new Date(end_date);
  const diff_time = new_end_date.getTime() - today.getTime();
  const diff_day = Math.floor(diff_time / (1000 * 60 * 60 * 24));
  const format_today = Utilities.formatDate(today,'Asia/Tokyo', 'yyyy/MM/dd');
  const format_start_date = Utilities.formatDate(new_start_date,'Asia/Tokyo', 'yyyy/MM/dd');
  const format_end_date = Utilities.formatDate(new_end_date,'Asia/Tokyo', 'yyyy/MM/dd');
  
  return [format_today,format_start_date,format_end_date,diff_day]
}

```


## Slackからタスク期間伸ばせるようにする
これは、前述のようにタスクの期限が過ぎていてもコスト低く延期できるように考えたものです。
延期するハードルが低すぎるのも良くないんですが、今回は継続に一旦フォーカスしてるので目をつぶります。
方針として今回は、Slackの[ワークフロービルダー](https://slack.com/intl/ja-jp/help/articles/360035692513-%E3%83%AF%E3%83%BC%E3%82%AF%E3%83%95%E3%83%AD%E3%83%BC%E3%83%93%E3%83%AB%E3%83%80%E3%83%BC%E3%82%AC%E3%82%A4%E3%83%89")と[GAS(Google Apps script)](https://workspace.google.co.jp/intl/ja/products/apps-script/)を使ってNotionにタスク追加するようにしました。

流れとしては以下
 1. Slack使いながらタスクに追加したいものが生まれる
 1. メッセージに特定のスタンプを押す
 1. 押すとタスクを挿入する際の値を入力するワークフローが起動
 1. 入力して送信するとスプレッドシートにデータが入る
 1. スプレッドシートに入力されたことが起因でGASが起動
 1. Notionの値が更新される

**Slackでスタンプ押してフォーム送信する流れ**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/666097/6c39318f-8d06-b97c-40a9-5cf2054c2074.png)

**フォーム入力**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/666097/295db4a0-5b8c-e4e5-a46f-d82bee361900.png)

**スプレッドシートに値が入る**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/666097/4d43a0ab-b81b-1630-a2b3-62598f2d13fe.png)

**Notionのタスク期限のプロパティが更新されるコード**
```send_to_notion.js
function extendTask(e) {
  const my_sheet = SpreadsheetApp.getActiveSheet();
  const my_sheet_name = my_sheet.getSheetName();
  if (my_sheet_name == "extend_tasks"){
    result = getFromSheet(my_sheet)
  }
  const end_date = getEndDate(result[0])
  const extend_date = getExtendDate(end_date,result[1])
  Logger.log(extend_date);

  sendToNotion(result[0],extend_date)
}

function getFromSheet(my_sheet){
  const latest_entry_row = my_sheet.getLastRow();
  const url = my_sheet.getRange(latest_entry_row, 1).getValue().slice(-32);
  const extend_num = my_sheet.getRange(latest_entry_row, 2).getValue();
    // urlからidに変換する必要があるのでその処理
  const id = url.substr(0, 8) + "-" + url.substr(8, 4) + "-" + url.substr(12, 4) + "-" + url.substr(16, 4) + "-" + url.substr(20, 12)
  return [id,extend_num]
}

function getEndDate(id) {
  user_properties = setUserProperty();
  const request_url = "https://api.notion.com/v1/pages/" + id ;

  const headers = {
    "Content-Type" : "application/json",
    "Notion-Version" : "2022-06-28",
    "Authorization" : "Bearer " + user_properties.getProperty("NOTION_KEY"),
  };

  const request_options = {
    "method" : "get",
    "headers" : headers,
  }

    // APIに通信
  const response = UrlFetchApp.fetch(request_url, request_options);

  // resultsキーにページ情報のオブジェクトの配列が格納されている
  const result = JSON.parse(response.getContentText()).properties["end_date"].date;

  let end_date = null
  if (result != null){
    end_date = result['start']
  }

  return end_date;
}


function sendToNotion(id,extend_date){
  user_properties = setUserProperty();
  const request_url = "https://api.notion.com/v1/pages/" + id ;
  Logger.log(request_url);
  const headers = {
    "Content-Type" : "application/json",
    "Notion-Version" : "2022-06-28",
    "Authorization" : "Bearer " + user_properties.getProperty("NOTION_KEY"),
  };

  const payload = {
    "properties": {
      "end_date": {
        "date": {
            "time_zone":null,
            "start":extend_date,
            "end":null
          }
      }
    }
  }

  const request_options = {
    "method" : "patch",
    "headers" : headers,
    "payload" : JSON.stringify(payload)
  }

  UrlFetchApp.fetch(request_url, request_options)
}

function getExtendDate(end_date,extend_num){
  const new_end_date = new Date(end_date);
  const sub_date = new_end_date.setDate(new_end_date.getDate() + extend_num)
  const new_sub_date = new Date(sub_date);
  const extend_date = Utilities.formatDate(new_sub_date,'Asia/Tokyo', 'yyyy-MM-dd');

  return extend_date
}

```
# まとめと追加でやりたいこと
今回はタスク管理をより継続しやすくなるために色々努力した話でした。
もちろんタスク管理が継続されてもタスク消化の推進力がないと意味ないという点が難問ですが、今回で解決に近づいたということにしておこうと思います。

最後に今後の課題列挙して終わろうと思います。
最後まで読んでいただいてありがとうございました。

【今後の課題】
* スタンプからフォーム入力するのがめんどくさいのでよりスムーズにしたい
* urlをキーにしているが、通知のメッセージからid取れるようにしたい
* タスクの優先順位を計算して取れやればいいのか教えてくれるアルゴリズム実装したい