kintoneプラグインのカレンダーPlusのリソース別スケジュール管理機能をカスタマイズする

12/25 コードを一部修正しました。

[カレンダーPlus Advent Calendar 2020](https://adventar.org/calendars/5414) の12/23担当分です。

#目次
[0.はじめに](#0-はじめに)
[1.カレンダーPlusとは？](#1-カレンダーplusとは)
[2.カレンダーPlus JavaScript APIを使う](#2-カレンダーplus-javascript-apiを使う)
[3.FullCalendarについて](#3-fullcalendarについて)
[4.注意事項](#4-注意事項)
[5.カスタマイズ](#5-カスタマイズ)
[6.課題](#6-課題)
[7.おわりに](#7-おわりに)

# 0. はじめに
実はQiita初投稿です。
はじめまして。
札幌でkintoneを活用した業務改善ソリューションを提供している、株式会社インセンブルの濱内です。
最近はコードを書くことも少なくなってきているのですが、最近お客様の希望を叶えるために行ったカレンダーPlusのカスタマイズをご紹介します。
具体的には、リソース別スケジュール管理機能の#月/#週の表示において、日付が記載されているヘッダ部分にその日の件数を表示させてみます（ニッチすぎる内容。。。。）
なお、DOMを操作するカスタマイズですので、サポートの対象外であり、バージョンアップなどにより動作しなくなるリスクなどがありますことをご了承ください。

# 1. カレンダーPlusとは？
※kintoneの説明はここでは省略します。
カレンダーPlusは[ラジカルブリッジ](https://radical-bridge.com/)が開発・販売を行っている、kintoneプラグインです。
kintone標準のカレンダー表示は機能に乏しく、実用性は厳しいものがあります。
それをステキに機能強化してくれるプラグインです。


## Basic版とPro版
カレンダーPlusには、Basic版とPro版が存在します。

Basic版は月別/週別/日別表示ができる、Googleカレンダーのような使い勝手を実現してくれるものです。
![スクリーンショット 2020-12-23 15.53.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25187/33aab343-c260-d7b7-e505-6af367e7f01f.png)
Pro版は、リソース別スケジュール管理機能が使えるようになり、担当者や会議室別など、最大5軸を自由に切り替えてスケジュール表示できるというものです。
![スクリーンショット 2020-12-23 15.55.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25187/1173dac8-c7df-30a2-d0d6-fc713a3cc345.png)

### 試用期間無制限で使うことができるという太っ腹なプラグイン。
試用期間中は1回カレンダー上で操作するたびにうざいAlertポップアップが出てくるだけです。
その試用期間でじっくり動作を検証しましょう。
そして、購入はなんとライセンス買い切り！（※日本国内。海外は国によってはサブスクの料金体系があるようです）
バージョンアップによって機能強化が続けられていますが、自分でアップデートすれば新機能も使うことができます。

ちなみに、最近、カレンダーPlusエバンジェリスト制度ができまして、私もエバンジェリストに任命していただきました。

# 2. カレンダーPlus JavaScript APIを使う
カレンダーPlusにはカレンダーPlus用のJavaScript APIが用意されています。
これを使うことで、動作順序を保障した処理が可能になり、安全にカスタマイズを行うことができます。
ただし、今はまだeventは結構限られている印象です。
マニアックなカスタマイズを行おうとすると、実現できないこともあります。
今後の充実に期待しましょう。

APIリファレンスは[こちら](https://radical-bridge.com/product/kintone-plugin/calendarPlus/CalendarPlus-JavaScript-API.pdf)。

例えばこのように書きます（リファレンスP.9より引用）

```
kintone.events.on('app.record.index.show', function(e) {
  calendarplus.events.on('cp.calendar.show', function(event) {
    alert("カレンダーが表示されました");
  });
});
```

kintone JavaScript APIみたいですよね？
kintone開発者なら習得コストが極小で書けることでしょう。

カレンダーPlusアドベントカレンダー12/19の記事でrex0220さんが更に詳しく記載されていますので、そちらをご参照ください。
https://qiita.com/rex0220/items/ac9077762f29d2c3d4a1

# 3. FullCalendarについて
カレンダーPlusは、FullCalendarというJavaScriptのライブラリを用いて開発されています。
https://fullcalendar.io/

この記事を書いている今日現在、最新版はv5ですが、カレンダーPlusが使用しているバージョンはv3となります。
カレンダーPlusはv3でだいぶ作り込まれているので、FullCalendarのバージョンが上がる予定は今のところは無さそうです。

カレンダーPlus JavaScript APIでは、FullCalendarのviewオブジェクトを取得することができ、viewオブジェクトを介して操作することが可能です（開発元のサポート対象外となります）

# 4. 注意事項
* クラス名などを用いてDOMを操作しています。kintoneおよびカレンダーPlusのアップデートによって動作しなくなる可能性があります。
* 同じ理由で、kintoneおよびカレンダーPlusの動作に影響を与える可能性があります。

# 5. カスタマイズ
冒頭にも記載しましたが、今回やりたいのは、リソース別管理機能の表示時に、日毎のヘッダ部分に、その日の予定数を記載したいというものです。
予定の管理として、予定数がひと目で把握できると便利ですよね。多分。

今回対応するのは「#月」表示と「#週」表示とします。

## カレンダーPlusの「カレンダー画面の描画後イベント」を登録する
カレンダーPlus JavaScript APIで使用できるイベントハンドラーを参照します。
今回はカレンダー表示に情報を付加しますので、カレンダー画面の描画後イベントを使用します。
登録は下記のようなコードとなります。

```javascript

  kintone.events.on("app.record.index.show", function(e) {
    if (e.viewType !== 'custom') return e;
    calendarplus.events.on('cp.calendar.show', function(event) {
      // #月表示/#週表示に対応
      if (event.view.type === 'timelineWeek' || event.view.type === 'timelineMonth') {

        /* 今回の処理 */

      }
    });
    return e;
  });
```

ポイントは、「#月」表示と「#週」表示の判定です。
eventハンドラーで取得できるviewオブジェクトのtypeプロパティに格納されている値を参照することで可能です。
typeプロパティの値はFullCalendarのドキュメントを参照し、カレンダーPlusの表示モードと対応させると下記のようになっています。

|カレンダーPlusの表示モード|プロパティの値|
|---|---|---|
|#年| timelineYear |
|#月| timelineMonth |
|#週| timelineWeek |
|#日| timelineDay |
参考：https://fullcalendar.io/docs/v3/timeline-view

## 日付リストの作成
event.recordsをすべて参照し、日毎の数を計算します。
日付のリストを作成しておきましょう。FullCalendarのDOMを参照して作成しました。
![プレゼンテーション1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25187/51e9726a-88e6-2a91-79ee-b64967931121.jpeg)

```javascript
          // 日付リスト作成
          event.view.el.find('th').each(function (k, v) {
            if ($(v).attr('data-date')) {
              dateList.push($(v).attr('data-date'));
            }
          });
```

## コード全体
以下の処理はコード全体を参照ください。

```javascript
jQuery.noConflict();
(function($) {
    "use strict";
    
    // カレンダーの開始日時・終了日時のフィールド名を設定
    const cpCustomConfig = {
      startFieldCode: "開始日",
      endFieldCode: "終了日"
    }
    var records = {}; // カレンダー上で更新される最新のレコード情報を格納する
    

    kintone.events.on("app.record.index.show", function(e) {
      if (e.viewType !== 'custom') return e;
      
      for (const record of e.records) {
        records[record['$id'].value] = record;
      }

      // イベントレコード描画時イベントのrecordオブジェクトを格納して最新のrecord情報を保持する
      calendarplus.events.on('cp.event.show', function (event) {
        if (event.view.type === 'timelineWeek' || event.view.type === 'timelineMonth') {
          records[event.record['$id'].value] = event.record;
          render(records, event);
        }
      });

      return e;
    });

    function render(records, event) {
      const dateList = [];
      
      // 日付リスト作成
      event.view.el.find('th').each(function (k, v) {
        if ($(v).attr('data-date')) {
          dateList.push($(v).attr('data-date'));
        }
      });
      
      const countList = []; // 日ごとの予定数を格納
      
      // 対象日のスケジュールをカウント
      for (const currentDate of dateList) {
        const currentDateM = moment(currentDate);
        let count = 0;
        
        for (const key in records) {
          // 開始日・終了日 未設定は対象外
          const record = records[key];
          if (record[cpCustomConfig.startFieldCode].value == null || record[cpCustomConfig.endFieldCode].value == null) continue;
          
          const startDateM = moment(record[cpCustomConfig.startFieldCode].value, 'YYYY-MM-DD');
          const endDateM = moment(record[cpCustomConfig.endFieldCode].value, 'YYYY-MM-DD');
          
          if (startDateM.isSame(endDateM)) {
            // From Toが同じ日の場合
            if (startDateM.isSame(currentDateM)) {
              count++;
            }
          } else if (
            // 終了日が翌日0:00で登録されているため、momentで1日戻す
            !(startDateM.isBefore(currentDateM, 'day') && endDateM.subtract(1, 'd').isBefore(currentDateM, 'day')) &&
            !(startDateM.isAfter(currentDateM, 'day') && endDateM.subtract(1, 'd').isAfter(currentDateM, 'day'))
          ) {
            count++;
          }
        }
        countList[currentDate] = count;
      }

      // 数量表示を削除
      event.view.el.find('th .cp-custom-count').remove();
      
      // 数量表示の挿入
      event.view.el.find('th').each(function (k, v) {
        if ($(v).attr('data-date')) {
          const dateStr = $(v).attr('data-date');
          const $elem = $('<span class="cp-custom-count">(' + countList[dateStr] + ')</span>');
          $(v).append($elem);
        }
      });
    }
})(jQuery);
```

## できあがり
下記のようになります！地味ですが、役立ちそうですね！？
![スクリーンショット 2020-12-23 17.30.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25187/c0dd3f72-35d6-65d7-aced-58cc1d906c66.png)

# 6. 課題
* 言うまでもありませんが、DOMを操作してますので、アップデートなどで動かなくなるリスクがあります。ただし、FullCalendarのバージョンが上がる可能性は直近では低いと思われますし、kintoneアップデートの影響を受けるようなカスタマイズはしてないので、低リスクとは考えています。
* 開始/終了は、日付前提で作成しています。日時の場合は更に分岐が必要でしょう。
* デバッグ足りない気がするので、不具合を見つけた方はこっそり教えて下さい。。。

# 7. おわりに
実は記事を書き始めてから致命的なバグに気づいてしまい、逃げの仕様に変更したりしたのですが、なんとか完成に漕ぎ着けました。イケてないところを見つけた方は教えて下さい🙇‍♂️

カレンダーPlus JavaScript APIのイベントがもっと増えると、カスタマイズの可能性がさらに広がりますね！
みなさんもカスタマイズを考えてみて、このイベントが欲しい！などとじゃんじゃんリクエストしてみましょう。