Trelloも要らない？ Exmentでカンバンボードを運用してみる

[BacklogやRedmineが要らないとか煽り記事書いてしまった](https://qiita.com/kohki-shikata/items/dfb50d48d423279b640e)筆者です。どうもです。

さて、[Exment](https://qiita.com/tags/exment)でガントチャートを作ってしまったら、どうせならカンバンボードもやってみたくなるのが人情ではないでしょうか？

というわけで、やってみました。

## Expressでフロントを作る

[簡易原価計算](https://qiita.com/kohki-shikata/items/7af13b1ac8c5be876243)の時や、ガントチャートの時と同じく、Express + Pugでフロント画面を作ります。

今回、フロントでは[Sortable.js](https://github.com/SortableJS/Sortable)というライブラリを用いました。HTMLの要素を自由にドラッグアンドドロップできるようになる魔法のようなライブラリです。

まずは、タスク表示のGET部分です。

```app.js
app.get('/kanban', async(req,res) => {
  ;(async () => {
    const exmentToken = JSON.parse(await fs.readFileSync('./exment_tokens.txt')).access_token
    let tasksData = await axios.get('https://example.com/api/data/tasks/?orderby=start_at', {
      headers: {
        'Authorization': 'Bearer ' + exmentToken
      }
    })
    .then(res => { return res.data.data })
    .catch(err => { console.error(err) })
  
    let tasksArray = []
    tasksData.forEach(item => {
      const id = item.id
      const name = item.value.title
      const status = item.value.status
      tasksArray.push({
        id: String(id),
        name: name,
        status: status,
        endAt: item.value.end_at
      })
    })

    let statuses = await axios.get('https://example.com/api/column/86', {
      headers: {
        'Authorization': 'Bearer ' + exmentToken
      }
    })
    .then(res => { return res.data.options.select_item_valtext })
    .catch(err => { console.error(err) })

    statuses = statuses.split('\r\n')
    let statusData = []
    statuses.forEach(status => {
      status = status.split(',')
      statusData.push({
        id: status[0],
        title: status[1]
      })
    })

    // console.log(statusData)
    
    let payload = []
    let i = 1
    statusData.forEach(item => {
      const tasks = _.filter(tasksArray, { status: item.id })
      let j = 1
      const taskItems = () => {
        let taskItems = []
        tasks.forEach(item => {
          taskItems.push({
            id: `item-id-${j}`,
            exmentId: item.id,
            title: item.name,
            endAt: item.endAt
          })
          j++
        })
        return taskItems
      }
      payload.push({
        id: `board-id-${i}`,
        title: item.title,
        item: taskItems()
      })
      i++
    })

    // console.log(payload)

    res.render('kanban', { payload })
  })()

})
```

今回も、Exment側からデータを取得して、Sortableのデータ形式に整形しています。なんというか、JSONコネコネ屋さんって感じですね…

続いて、viewです。

```kanban.pug
<!DOCTYPE html>
html(lang="ja")
  head
    meta(charset="UTF-8")
    meta(name="viewport", content="width=device-width, initial-scale=1.0")
    title Document
    .
      <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css" integrity="sha512-NhSC1YmyruXifcj/KFRWoC561YpHpc5Jtzgvbuzx5VozKpWvQ+4nXhPdFgmx8xqexRcpAglTj9sIBWINXa8x5w==" crossorigin="anonymous" />
    style.
      * {
        box-sizing: border-box;
      }
      [class^="wrapper-"] {
        background: #ddd;
        padding: 0;
        width: 300px; 
        margin-right: 30px;
        flex: 0 0 auto;
        height: 100%;
      }
      [id^="board-id-"] {
        padding-left: 0;
        padding: 15px;
      }
      #boards {
        width: 100vw;
        overflow-x: scroll;
        display: flex;
        height: 100%;
      }
      .title {
        margin-top: 15px;
        text-align: center;
        margin-bottom: 0;
      }
      .item {
        background: #f2f2f2;
        list-style-type: none;
        padding: 15px;
        margin-bottom: 15px;
        margin-left: 0;
      }
      .item:last-child {
        margin-bottom: 0;
      }
      .due {
        font-size: 11px;
        color: #777;
        font-weight: bold;
      }

        
  body
    #boards
      each board in payload
        div(class="wrapper-" + board.id)
          h2.title=board.title
          ul(id=board.id).sortable
            if board.item
              each item in board.item
                li.item(data-exmentid=item.exmentId)
                  =item.title
                  br
                  span.due=`期日：${item.endAt}`

    .
      <script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.10.2/Sortable.min.js" integrity="sha512-ELgdXEUQM5x+vB2mycmnSCsiDZWQYXKwlzh9+p+Hff4f5LA+uf0w2pOp3j7UAuSAajxfEzmYZNOOLQuiotrt9Q==" crossorigin="anonymous"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.20.0/axios.min.js" integrity="sha512-quHCp3WbBNkwLfYUMd+KwBAgpVukJu5MncuQaWXgCrfgcxCJAq/fo+oqrRKOj+UKEmyMCG3tb8RB63W+EmrOBg==" crossorigin="anonymous"></script>
    script.
      var boards = document.querySelectorAll('.sortable')
      var i = 1
      boards.forEach(function(board) {
        new Sortable(board, {
          group: 'status',
          onEnd: function(event) {
            var newStatus = event.to.id.replace(/[^0-9]/g, '')
            var itemId = event.item.dataset.exmentid
            axios.put('/kanban', {
              id: itemId,
              status: newStatus
            })
          }
        })
      })
      
```

これで`http://localhost:3000/kanban`にアクセスしてみましょう。

![2020-09-16_10h24_21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116223/90c7e2aa-62f9-f8fd-0a9e-2b216640c58e.png)

こんな感じになっていれば成功です！

なお、Trello風のUIを実現するCSSは、以下の記事を参考にしました。

- [いまどきのCSSではどう書けるか？ Trelloのレイアウトを再現してみた](https://www.webprofessional.jp/building-trello-layout-css-grid-flexbox/)

## カンバン間を移動したらステータスが変更されるようにPUTしよう

ガントチャートの時もそうでしたが、フロントで弄った時に、きちんとバックエンドに通信が飛んでデータが上書きされてほしいですよね。

Sortable.jsにもコールバックの仕組みがきちんと用意されているので、以下の技術を追記します。

```kanban.pug
script.
  var boards = document.querySelectorAll('.sortable')
  var i = 1
  boards.forEach(function(board) {
   new Sortable(board, {
      group: 'status',
      onEnd: function(event) {
        var newStatus = event.to.id.replace(/[^0-9]/g, '')
        var itemId = event.item.dataset.exmentid
        axios.put('/kanban', {
          id: itemId,
          status: newStatus
        })
      }
    })
  })
```

オプションのプロパティ`onEnd`が、ドラッグアンドドロップを完了した時のコールバックです。
ここにExment側のAPIから持ってきたExmentのタスクのIDと、ボードのIDを取り込みます。

SortableのボードのIDは、`board-id-1`みたいな感じなので、数字以外を取り払ってあげます。Exmentで設定した列設定では、enum型で値は数字を列挙しているので、ID番号＝Exment側の値となります。

最後にaxiosで、Express側にデータを投げます。

Express側はこんな感じです。

```app.js
app.put('/kanban', async (req, res) => {
  ;(async () => {
    const exmentToken = JSON.parse(await fs.readFileSync('./exment_tokens.txt')).access_token

    // console.log(req.body)

    await axios.put('https://example.com/api/data/tasks/' + req.body.id, {
      value: {
        status: req.body.status
      }
    }, {
      headers: {
        'Authorization': 'Bearer ' + exmentToken
      }
    })
    .then(res => { return res })
    .catch(err => { console.error(err.response.data) })
  })()
})
```

シンプルですね。タスクのIDをエンドポイントに付加して、PUTします。statusも、先程のボードのID番号を指定してあげれば、ステータスの状態と連動します。

## 動作確認してみよう

Exmentにアクセスし、カンバン上で動かす予定のタスクを確認しておきます。未着手になってますね。

![2020-09-15_15h04_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116223/14e5a69d-affa-f4d9-6344-2c74508cd3ca.png)

カンバン上でドラッグ＆ドロップしてみます。

![2020-09-16_10h19_17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116223/229a8eaf-2320-8593-1b3f-f7806a7abd97.png)


ドロップ後、リロードしてみましょう。

![2020-09-16_10h22_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116223/68551df9-559d-7dae-1ce2-84384583722b.png)


また、Exment側もリロードして確認してみましょう。

![2020-09-15_15h07_39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116223/a200b616-8270-b63f-235d-020799583a41.png)

きちんとステータスが変わっているのを確認できました！

## 課題もあります

まあ、ガントチャートの時ほどではないのですが、課題はあります。

### 縦方向の並びを変えるのが難しい

無理じゃないとは思うんですが、今回はExment側に並び順のフィールドを作ってないので、並べ替えられません。並べ替えも、並べ替えごとに番号を全部のタスクで書き換えないといけないので、API的にも負荷は大きそうです。

## まとめ

というわけで、いかがでしたでしょうか。

この実装ではTrelloほど高機能な実装はしませんでしたが、簡易とはいえ、Exmentでガントチャートも、そしてカンバンも出来るとなると、これでタスク管理したくなってきませんか？　もちろん、もっと作り込めば、Backlog/Redmine/Trelloに負けないタスク管理サービスを作ることも可能です。

興味持った方は、ぜひトライしてみてください！