カレプラを彩る、素敵なカレンダーPlus API

 カレンダーPlus Advent Calendar 2020 の16日目の記事です。

こんにちは、かりんこラボの坂本です。
カレンダーPlusの好きなところは「kintoneに素敵なカレンダー機能をプラスするプラグイン」という説明文です!
そこ??と思われるかもしれないのですが、「高機能なカレンダー」ではなく「素敵なカレンダー」という言葉が使われているところがステキだと思うのです。

さて、そんな素敵なカレンダーPlusとの連携機能を、かりんこラボの日付計算プラグインに追加しました。
このプラグインにカレンダーPlus連携を追加できたのは カレンダーPlus API のおかげです。
この記事では、私がカレンダーPlus APIを使ってみてハマりそうだなと思ったところと、APIを使ったカスタマイズの例を書きます。

カレンダーPlus APIに興味がある方、これから使ってみたい方へ

カレンダーPlusのAPIの考え方・使い方はkintone本体のAPIと似ているので、kintoneのJavaScriptカスタマイズを経験された方には、それほど違和感なく使えると思います。

カスタマイズを始めるにあたっては、Calendar Plus JavaScript API リファレンスをみて、タイミング(イベント)を理解しましょう。
自分でプログラムを書けなくても、この「タイミング」だけでも知っておくと、プロにカスタマイズを頼んだり、連携サービス・プラグインなど使ったりするときに、やりたいことのイメージが具体化しやすくなると思います。

APIの使い方説明について書きたい気持ちもあるのですが、今回は、カスタマイズ始めたての時に引っかかりそうだなと思うところをいくつかあげてみました。

「return」を忘れずに

イベントの終わりに、カレンダーPlusのeventをリターンするか、falseをリターンするかで挙動が変わります。
更新系のイベントでうっかりreturnを書くのを忘れてしまうと、falseのリターンと同じになり、更新されなくなるので気をつけましょう。
コーディングが長くなるとreturnが書かれていないことに気が付きにくくなるので、注意です。

「タイトル入力ダイアログからのイベントレコード新規保存前イベント」はちょっと違う

カレンダーPlusのイベントオブジェクトのプロパティにはrecordrecord-originalがあって、カレンダーPlus関連フィールド「開始日時、終了日時、終日指定、タイトル」以外の 他フィールドの内容もどちらかのプロパティで取得できます。
ただし、「タイトル入力ダイアログからのイベントレコード新規保存前イベント」だけは例外で、recordから取得できるのは、カレンダーPlusで更新するフィールドのみ。
もし、このイベントで他フィールドに対して何かしたい場合は、recordオブジェクトに自分で追加するとか、REST APIを使ってフォームの設定を取得するなど、他のイベントとは違う操作が必要になります。

「view」プロパティはちょっと難易度高め?

レコード操作については、カレンダーPlusイベントオブジェクトの「record」プロパティや「record-original」プロパティ、またはkintone REST APIを使用するため、とても扱いやすいです。
一方、レコード以外の情報であるカレンダービューの内容は、カレンダーPlusイベントオブジェクトのプロパティviewを使います。
ただし、このviewを扱い方は、kintoneおよびカレンダーPlus関係リファレンスには載っていないので、「FullCalendar」のリファレンスや、デバッグしながらどういった情報が取れるのか調べる必要があり、少し面倒かもしれません。

エラー時は発生源の確認をしましょう

カレンダーPlusのカスタマイズJavaScriptに問題があると、カレンダーPlus本体のもともとの機能が動かなくなる場合があります。
これは、自分で追加したカスタマイズだけでなく、カレンダーPlusに連携しているプラグインやサービスなどでも、その設定を誤ると起こる可能性があります。
カレンダーPlusで何かエラーメッセージが表示されたり、更新が上手くできなくなったりした場合は、「カレンダーPlusのエラー」と一纏めにせずに、カレンダーPlus JavaScrpitのカスタマイズや連携プラグインなど、どこに原因があるかを確認しましょう。
すべてのカスタマイズと他プラグインを外してカレンダーPlusのみで動作確認 → 問題なければ1個ずつ戻してどこでエラーがでるか確認といった手順で確認することができます。

カレンダーPlus API活用サンプル:勤務予定データ一括登録

カレンダーPlus APIというものがあって、何かが出来るということは分かるけど、具体的にどんなことができるかイメージできないという方もいらっしゃると思います。
すでにカレンダーPlus Advent Calendar 2020でもいくつかカスタマイズ例が公開されていますが、私も1つ活用サンプルを作りましたので、参考になればうれしいです。

シナリオ

利用者が勤務予定を登録する「勤務予定表アプリ」です。
月初に、各利用者が自分の1ヶ月間の勤務予定(通常の勤務時間は 9:00〜17:00)を作成します。

カレンダーPlus標準またはkintone標準で画面上でレコードを作る場合は、まず9:00〜17:00で1件目のレコードを作成して、複製機能を使って日付を変えながら1ヶ月分作成していくことになります。

週休2日としても、約20日分を画面上で繰り返し複製して作成していくのはちょっと面倒ですよね。
ここで、カレンダーPlus APIを使ったカスタマイズで「ボタンクリックで一括登録」の機能を追加してみます。

アプリ構成

【アプリフォーム】

「レコード種別」の使い方・・・通常の予定データは「レコード」で登録、ボタン表示に使うダミーデータを「ボタン」で登録。

【カレンダーPlusの設定】

動作イメージ

事前に勤務予定表アプリに登録しておくレコード

氏名_タイトル用 → 「今月の予定一括登録」
業務開始日時&業務終了日時 → 2020/12/01
終日 → チェックON
レコード種別 → ボタン

  1. カレンダーPlusの月別表示カレンダーを開くと、事前登録したレコード種別「ボタン」のレコードがボタンとして表示されます。
    他レコードと区別しやすいようにCSSでボタンっぽくデザインを変更しています。
  2. このボタンをクリックすると、ボタンが表示されている1ヶ月のうちの営業日(カレンダーPlusで設定した営業日)を判定して、9:00-17:00でログインユーザーの予定データを一括登録します。

カスタマイズはこんな感じ

JavaScriptとCSSは以下のように書きました。これ以外にURL指定で以下のJavaScriptとCSSファイルも追加が必要です。
Luxonは日付操作、SweetAlert2はアラート表示に使います。

https://js.cybozu.com/luxon/1.25.0/luxon.min.js
https://js.cybozu.com/sweetalert2/v10.10.4/sweetalert2.min.js
https://js.cybozu.com/sweetalert2/v10.10.4/sweetalert2.min.css

イメージを掴んでいただくためのサンプルなので、実運用で使うための考慮はしていません。
内容の細かい説明は省きますが、どのイベントでどんなことをしているか、ざっくりわかるようにコメントを書いています。

(function () {
    'use strict';

    /**
     * 一括登録用のレコードを作成
     * @param {*} formattedDate 
     */
    const generateRowData = function (formattedDate) {
        const rec = {
            '業務開始日時': {
                'value': formattedDate + 'T09:00:00+09:00'
            },
            '業務終了日時': {
                'value': formattedDate + 'T17:00:00+09:00'
            },
            '氏名_タイトル用': {
                'value': kintone.getLoginUser().name
            },
            'ユーザー': {
                'value': [
                    {
                        'code': kintone.getLoginUser().code
                    }
                ]
            }
        };
        return rec;
    };

    /**
     * kintone レコード一覧画面の表示後イベント
     */
    kintone.events.on('app.record.index.show', function (event) {

        /**
         * カレンダーPlus イベントレコード描画時イベント
         */
        calendarplus.events.on('cp.event.show', function (e) {
            if (e.record['レコード種別'].value === 'ボタン') {
                // 他レコードと区別する為、レコード種別がボタンの場合はボタンっぽいデザインに変更
                const elm = e.element;
                elm[0].classList.add('calendar-button');
            }
            return e;
        });

        /**
         * カレンダーPlus マウスドラッグによるイベントレコード更新前イベント
         */
        calendarplus.events.on('cp.event.edit.submit', function (e) {
            if (e['record-original']['レコード種別'].value === 'ボタン') {
                // レコード種別がボタンの場合はレコード更新キャンセル(ボタンが動かされるのを禁止)
                return false;
            } else {
                return e;
            }
        });

        /**
         * カレンダーPlus イベントレコードクリックによる詳細画面遷移前イベント
         */
        calendarplus.events.on('cp.event.click', function (e) {
            if (e.record['レコード種別'].value !== 'ボタン') {
                return e;
            }
            // SweetAlert2の設定
            const options = {
                icon: 'question',
                html: kintone.getLoginUser().name + 'さん、<br>' + e.view.title + 'の予定を一括登録しますか?',
                showCancelButton: true,
                confirmButtonText: 'はい',
                cancelButtonText: 'いいえ'
            };
            // ボタンがクリックされたらメッセージを表示
            Swal.fire(options).then(function (result) {
                // 「はい」が選ばれた場合
                if (result.isConfirmed) {
                    // カレンダーPlusイベントオブジェクトのviewプロパティから情報取得
                    // 表示されている月の開始日取得
                    const startDate = luxon.DateTime.fromISO(e.view.currentRange.start.toISOString());
                    // 表示されている月の終了日取得
                    const endDate = luxon.DateTime.fromISO(e.view.currentRange.end.toISOString());
                    // 開始日と終了日から日数判定
                    const numDays = endDate.diff(startDate, 'days');
                    // 営業日取得(0:日曜,1:月曜・・・)
                    const bussinessDays = e.view.options.businessHours.dow;

                    // 登録用データ作成
                    const insertRecords = [];
                    for (let i = 0; i < numDays.values.days; i++) {
                        const insertDate = startDate.plus({ 'days': i });
                        // 営業日の場合に追加するレコード作成(setDate.weekdayは 1:月曜,2:火曜,・・・.7:日曜)
                        if (bussinessDays.indexOf(insertDate.weekday.toString()) !== -1 ||
                            (insertDate.weekday === 7 && bussinessDays.indexOf('0') !== -1)) {
                            const formattedDate = insertDate.toFormat('yyyy-MM-dd');
                            const rowdata = generateRowData(formattedDate);
                            insertRecords.push(rowdata);
                        }
                    }
                    // REST APIで一括登録
                    const body = {
                        'requests': [
                            {
                                'method': 'POST',
                                'api': '/k/v1/records.json',
                                'payload': {
                                    'app': kintone.app.getId(),
                                    'records': insertRecords
                                }
                            }
                        ]
                    };
                    kintone.api(kintone.api.url('/k/v1/bulkRequest', true), 'POST', body, function (resp) {
                        // 成功時は画面をリロード
                        location.reload();
                    }, function (error) {
                        Swal.fire('登録失敗!', '', 'error');
                    });
                }
            });
            // 詳細画面に遷移するのをキャンセル
            return false;
        });

        return event;
    });
})();
@charset "UTF-8";

.calendar-button {
  height: 35px;
  width: 150px;
  background: #fdc689 !important;
  line-height: 37px !important;
  text-align: center;
  box-shadow: 1px 2px 4px #d4842c;
}
.calendar-button:active {
  -ms-transform: translateY(2px);
  -webkit-transform: translateY(2px);
  transform: translateY(2px);
  box-shadow: none;
}

CSS参考:https://www.yuu-diaryblog.com/2017/04/22/css-button-2/

まとめ

カレンダーPlus API があることで、さらにカレンダーPlusを拡張させることができるイメージが伝わりましたでしょうか?
今の時点では、まだカレンダーPlus APIを使用したサンプルや事例が少ないのですが、これからどんどん増えてくる気がしています。
無理して難しいカスタマイズを追加する必要はないと思いますが、頭の片隅に少しAPIの知識があると、活用を考える上で役立つことがあると思います。

また、カレンダーPlus アドオンのようなカレンダーPlusプラグインのプラグインもこれから出てくるかも?という期待もあって(かりんこラボからも出したい!)ますます楽しみが広がります。

ぜひ皆さんも、カレンダーPlus Advent Calendar 2020で他の方が書かれている記事も参考にして、楽しみながらカレンダーPlusの活用方法を考えてみてくださいね。
色々なアイディアで、kintoneに素敵なカレンダー機能をプラスするカレンダーPlusプラグインがもっと素敵になりますように!!

Follow me!