Asana+GAS+DataPortalで子供の夏休みの宿題の進捗を可視化する

# はじめに
僕に似て夏休みの宿題をサボる傾向にあるようなので、この形質を遺伝させるのはまずいと思い、バーンダウンチャートもどきを作りました。

こんな感じ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12060/8e7972ba-2517-a2f9-8572-ead563266fc4.png)

# アーキテクチャ
asanaのタスクをGASでsheetに書いてdataportalでグラフ化する流れです。
asanaの[Portfolio](https://asana.com/ja/guide/team/onboard/portfolio-management)という機能でサクッとバーンダウンチャートを作成できるっぽいんですが、有料なので自分で書くのです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12060/16890a48-2338-ee03-a10a-45112835b326.png)

# やったこと
## 大変ポイント(ストーリーポイント)をつける

最初に子供と話し合ってこんな感じでポイントをつけました。
進捗を定量化することが目的なので割とテキトウに付けています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12060/f05561a0-2429-5ee2-27a3-1d5d4784a2e7.png" width=50%/>

## Asanaにタスクを作る
決めた単位だけブワーッと作ります。うちの場合は一旦スプレッドシートでcsvを作ってasanaにimportしました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12060/1d584526-2f1e-8d26-62be-731d9d5060f9.png" width=50% />

中身のdescriptionはこんな感じにします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12060/74b8b221-3c47-78fa-b37f-5498ca5e33e8.png" width=50% />

`point: 1`は先ほど決めたポイントです。これは後でGASでparseしてゴニョゴニョします。

## GASを書く
これをスケジュール実行(1hくらい？)で仕掛けておきます。

<details>
<summary>コード内容</summary><div>

```
// main.gas

const token = "asana token"
const groupId = "asana project group id"
const spreadSheetId = "集計用のスプレッドシートのid"
const start =  new Date("2021-07-21") // 夏休み初日
const end = new Date("2021-08-31") // 夏休み最終日
const allPoint = 295 // 大変ポイントの総計

function exec() {
  const asana = new Asana(token)
  const spreadSheet = new SpreadSheet(spreadSheetId, "list")

  const list = asana.getTasks(groupId)
  const headers = Object.keys(list[0])
  spreadSheet.apply(headers, list)

  const list2 = _summarize(list, start, end, allPoint)
  const spreadSheet2 = new SpreadSheet(spreadSheetId, "summary")
  spreadSheet2.apply(Object.keys(list2[0]), list2)
}

function _summarize(list, start, end, allPoint) {
  const range = __dateRange(start, end)
  const expected = allPoint / range.length
  const dates = range.map(date => {
    const dateStr = date.getFullYear() + "/" + (date.getMonth() + 1) + "/" + date.getDate()
    return {
      dateStr: dateStr,
      point: 0,
      expected: expected,
    }
  })
  return list.reduce((ac, val) => {
    if (!val.completed) {
      return ac
    }
    const date = new Date(val.completed_at)
    const dateStr = date.getFullYear() + "/" + (date.getMonth() + 1) + "/" + date.getDate()
    const di = ac.findIndex(v => v.dateStr === dateStr)
    if (di < 0) {
      ac.push({
        dateStr: dateStr,
        point: val.point,
        expected: expected,
      })
    } else {
      ac[di].point += val.point
    }
    return ac
  }, dates)
}

function __dateRange(startDate, endDate, steps = 1) {
  const dateArray = []
  const currentDate = startDate
  while (currentDate <= endDate) {
    dateArray.push(new Date(currentDate.getTime()))
    currentDate.setUTCDate(currentDate.getUTCDate() + steps)
  }
  return dateArray
}

//asana.gas

class Asana {
  constructor(token) {
    this.token = token
  }

  getTasks(groupId) {
    const options = {
      'method': 'get',
      'contentType': 'application/json',
      'headers': {
        'Authorization': 'Bearer ' + this.token,
      },
    }
    const params = [
      "name", "modified_at", "completed", "completed_at", "html_notes"
    ]
    const url = Utilities.formatString(
      `https://app.asana.com/api/1.0/projects/%s/tasks?opt_fields=%s`, groupId, params.join(","))
    const res = UrlFetchApp.fetch(url, options)
    if (res.getResponseCode() !== 200) {
      const errStr = Utilities.formatString("invalid status code!: %d, %s", res.getResponseCode, res.getContentText)
      throw new Error(errStr)
    }
    const data = JSON.parse(res).data

    const list = data.map(val => {
      const html = val.html_notes
      const point = parseInt(Parser.data(html).from("point:</strong> ").to("</body>").build())
      const completed = val.completed === "TRUE" ? true : false
      return {...val, point: point}
    })

    return list
  }
}

//spreadsheet.gas

class SpreadSheet {
  constructor(id, sheetName) {
    this.spreadSheet = SpreadsheetApp.openById(id)
    this.sheetNameData = sheetName
  }
  apply(headers, list) {
    const sheet = this.spreadSheet.getSheetByName(this.sheetNameData)
    const range = sheet.getRange(1, 1, 1, headers.length)
    range.setValues([headers])
    const list2 = list.map(obj => {
      return Object.values(obj)
    })
    sheet.getRange(2, 1, list2.length, headers.length).setValues(list2)
  }
}
```

</div></details>


## Dataportalでグラフを作る
下記のような感じで集計されたカラムのうち`dateStr`をディメンジョンにして、
`point`,`point`,`expected`,`expected`と2つずつカラムをフィールドに加えます。
さらに同じ種類のカラムのうち一方だけを`スタイル`で`累計`にチェックをするとバーンダウンチャートぽくなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12060/3b657e7a-bda5-4198-76a5-27cffe6543f5.png)

このグラフだと`目標累計`の線に`ポイント累計`が追いつくように進捗していけばいいことがわかります。
可視化できてよかったですね！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12060/c64892c4-e8e6-97d2-d161-4ecec2cde2ac.png)

# 最後に
はっきり言って僕がAsana apiを触りたかったので作ったんですが、うちでは「これだけ頑張ればこのくらいさぼれるよ」と教えるために使っています。決して子供へプレッシャーを与える目的で使ってはいけないなあと思いました。

以上