スケジュールで受付管理業務を実現する

目次

caution
警告

Moment.js はメンテナンスモードになり、 日付処理できる代替ライブラリへの移行 (External link) が推奨されています。
代替ライブラリのひとつ Luxon (External link) については、 kintone カスタマイズでの導入方法の紹介記事 があります。

はじめに

今回は、スケジュールのカスタマイズにより、「受け付け管理業務」を実現します。
想定するシナリオは次のとおりです。

  1. 「応対ユーザー」がお客様の来訪予定を登録する。

  2. お客様が定刻に来社する。

  3. 「受け付けユーザー」は、来訪予定からお客様のアポイントを確認する。

  4. 「受け付けユーザー」は、お客様の来社を「応対ユーザー」へ通知する。

  5. 「応対ユーザー」は、通知を確認し、お客様と面会する。

受け付け管理業務を実現する上での要件は次のようなものでしょう。

  • 受け付けユーザーがその日に来訪予定を一覧等で簡単に確認できる。
  • 応対ユーザーへの来訪通知が簡単に行える。

これらの Garoon で実現するために、次のような機能を作りこんでいきます。

  • 受け付けユーザーが来訪者を確認できるよう、打ち合わせ参加者として受け付けユーザーを強制的に自動設定する機能
  • 受け付けユーザーを参加者として含む、当日の来訪予定を一覧表示する機能(ポータル)
  • 簡単なクリック操作で来訪を通知する機能(コメント登録による通知)
  • その他便利機能(関連情報の照会、来訪登録の取消等)

前提事項と注意事項

  • このカスタマイズには、クラウド版 Garoon の環境が必要です。

完成イメージ

スケジュールとポートレットのカスタマイズで受け付け管理業務を実現します。

予定登録/編集時

受け付けユーザーのポータル

スケジュールの詳細

リソースの準備

受け付けユーザーの定義

受け付けユーザー情報は、ファイル管理に配置することにより、ポータル、スケジュールの双方から共有できるようにします。

  1. 受け付けユーザーとして使用するユーザーの情報を確認します。
    まず、Garoon システム管理画面の[各アプリケーションの管理]>[メール]>[ユーザーアカウント]をクリックします。
    • ログイン名:cybozu.com 共通管理の「ユーザーの管理」の[組織/ユーザー]をクリックし、ログイン名を確認します。

    • ユーザーID:該当するユーザーをクリックし、「ユーザーアカウント一覧」画面の URL を確認します。
      URL の uid=数字 の数字部分がユーザー ID です。

  2. 次の サンプルコード をテキストエディターに貼り付けます。
サンプルコード

3 行目と 5 行目を、 受け付けユーザーの定義 1. で確認した内容に置き換えます。ファイル名を「attendee.js」、文字コードを「UTF-8」で保存します。

1
2
3
4
5
6
// 来訪時に自動追加する参加者
const GAROON_SAMPLE_PROGRAM_ATTENDEE = {
  id: '1で確認したユーザーID',
  code: '1で確認したログイン名',
  type: 'USER'
};

受け付けユーザーの定義 1. の画面を例とすると、記載内容は次のようになります。

1
2
3
4
5
6
// 来訪時に自動追加する参加者
const GAROON_SAMPLE_PROGRAM_ATTENDEE = {
  id: '4',
  code: 'reception',
  type: 'USER'
};
information

サンプルのため、受け付けユーザーの定義はグローバル変数を使用しています。実運用の際は、CSV ファイルでの管理に変更するなどを推奨します。

ライブラリ

  • jQuery
    1. https://github.com/jquery/jquery/releases/tag/3.6.0 (External link) にアクセスします。
    2. 「Assets」の「Source code(zip)」をクリックし、zip ファイルをダウンロードします。
    3. zip ファイルを解凍します。
    4. 「dist」フォルダー内の jQuery.min.js を利用します。
  • SweetAlert2
    1. https://github.com/sweetalert2/sweetalert2/releases/tag/v11.0.16 (External link) にアクセスします。
    2. 「Assets」の sweetalert2.min.js と sweetalert2.min.css をクリックし、ファイルをダウンロードします。
    3. ダウンロードした sweetalert2.min.jssweetalert2.min.css を利用します。
  • Moment.js
    1. https://github.com/moment/moment/releases/tag/2.29.1 (External link) にアクセスします。
    2. 「Assets」の「Source code(zip)」をクリックし、zip ファイルをダウンロードします。
    3. zip ファイルを解凍します。
    4. 「min」フォルダー内の moment.min.jsmoment-with-locales.min.js を利用します。
  • Garoon html/css/image-Kit for Customize
    1. https://github.com/garoon/css-for-customize (External link) にアクセスします。
    2. [Code]ボタン >「Download ZIP」を選択し、zip ファイルをダウンロードします。
    3. zip ファイルを解凍します。
    4. 「css」フォルダー内の grn_kit.css を利用します。
  • css for SweetAlert on Garoon
    1. https://github.com/garoon/css-for-SweetAlert (External link) にアクセスします。
    2. [Code]ボタン >「Download ZIP」を選択し、zip ファイルをダウンロードします。
    3. zip ファイルを解凍します。
    4. 「css」フォルダー内の sweetalert_button_grn.css を利用します。

カスタマイズの適用

スケジュール参加者を自動設定する機能

  1. 次の サンプルコード をエディタにコピーして、ファイル名を「add-attendee.js」、文字コードを「UTF-8」で保存します。

  2. スケジュールにカスタマイズグループを追加し、JavaScript / CSS によるカスタマイズを適用します。
    カスタマイズグループを追加する手順は、 スケジュールのカスタマイズ (External link) を参照してください。

    適用するファイルおよびリンクは次のとおりです。

    • JavaScript カスタマイズ
      • jQuery:ダウンロードした jQuery.min.js
      • 受け付けユーザー定義:作成した attendee.js
      • スケジュール参加者を自動設定する機能:作成した add-attendee.js
    • CSS カスタマイズ:
      • Garoon html/css/image-Kit for Customize:ダウンロードした grn_kit.css

    カスタマイズグループを追加した後の画面は、次のようになります。

サンプルコード

ファイル名を「add-attendee.js」、文字コードを「UTF-8」で保存します。
ファイル名は任意ですが、ファイルの拡張子は「js」にしてください。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
/**
*
* スケジュールで受付管理業務を実現する
* Copyright (c) 2018 Cybozu
*
* Licensed under the MIT License
* https://opensource.org/license/mit/
*/

(function($) {
  'use strict';
  // 参加者を追加する予定メニュー
  const ADD_MENU = '来訪';
  // 受付時にタイトルに付加する文字列
  const ADD_TITLE = '【受付対応済み】';

  // 対応状況を更新するボタン名
  const BUTTON_COMPLETE = '対応完了';
  const BUTTON_UNCOMPLETE = '対応取消';

  const SCHEDULE_DATASTORE_KEY = 'jp.co.cybozu.schedule.sample.attendee';

  /**
  * 予定の登録、変更画面のの保存実行前イベント。
  * 予定メニューとして「来訪」を選択した際、受付ユーザを参加者として設定する
  */
  garoon.events.on(['schedule.event.create.submit', 'schedule.event.edit.submit'], (ev) => {
    const event = ev.event;

    // 予定メニューが「来訪」の場合
    if (event.eventMenu === ADD_MENU) {
      // 受付ユーザを参加者に追加する
      event.attendees.push(GAROON_SAMPLE_PROGRAM_ATTENDEE);

      let datastore = garoon.schedule.event.datastore.get(SCHEDULE_DATASTORE_KEY);
      // 予定の登録時、または編集時に予定メニューを「来訪」に変更したとき
      if (!datastore) {
        // カスタム項目(Schedule datastore)に初期値を設定
        datastore = {
          value: {
            isAccepted: false,
          },
        };
        garoon.schedule.event.datastore.set(SCHEDULE_DATASTORE_KEY, datastore);
      }
    }
    return ev;
  });

  /**
  * 予定の登録画面表示時イベント。
  * 再利用の場合、受付済フラグをクリアする。
  */
  garoon.events.on('schedule.event.create.show', (ev) => {
    const event = ev.event;
    let receptionUser;

    // 再利用のスケジュールで、「来訪」かつ受付ユーザを参加者として含む来訪の予定の場合、
    if (ev.reuse && event.eventMenu === ADD_MENU) {
      receptionUser = event.attendees.filter((attendee) => {
        return attendee.id === GAROON_SAMPLE_PROGRAM_ATTENDEE.id;
      });
      if (receptionUser) {
        // 【受付対応済み】をタイトルから削除
        event.subject = event.subject.replace(ADD_TITLE, '');
        // カスタム項目(Schedule datastore)を初期化
        const datastore = {
          value: {
            isAccepted: false,
          },
        };
        garoon.schedule.event.datastore.set(SCHEDULE_DATASTORE_KEY, datastore);
      }
    }
    return ev;
  });

  /**
  * 予定の詳細画面表示時イベント。
  * 受付対応状況を更新するボタンを配置する
  */
  garoon.events.on('schedule.event.detail.show', (ev) => {
    // 受付ユーザを参加者として含む来訪の予定の場合、受付実施/取消ボタンを配置
    const event = ev.event;
    const eventId = event.id;
    const datastore = garoon.schedule.event.datastore.get(SCHEDULE_DATASTORE_KEY);
    let button, receptionUser;

    if (event.eventMenu === ADD_MENU) {
      receptionUser = event.attendees.filter((attendee) => {
        return attendee.id === GAROON_SAMPLE_PROGRAM_ATTENDEE.id;
      });

      // 受付ユーザを含む場合のみ処理対象とする
      if (!receptionUser) {
        return ev;
      }
      button = $('<button>');
      button.attr('id', 'updateStatus');
      // Garoon html/css/image-Kit for Customizeによるボタンの装飾
      button.addClass('button_main_sub_grn_kit');

      // 対応実施状況によってボタン名称を切り替え
      if (datastore && datastore.value.isAccepted === true) {
        button.html(BUTTON_UNCOMPLETE);
      } else {
        button.html(BUTTON_COMPLETE);
      }

      if ($('#' + button.id)) {
        $(garoon.schedule.event.getHeaderSpaceElement()).append(button);

        // クリックイベント
        // 取消 ⇒ カスタム項目のクリア、タイトルから【受付対応済み】を除去
        // 完了 ⇒ カスタム項目の設定、タイトルに【受付対応済み】を付加
        button.on('click', function() {
          const currentEvent = garoon.schedule.event.get();
          let subject = currentEvent.subject;
          // 押したときのボタン名称によって更新内容を切り替え
          if ($(this).html() === BUTTON_COMPLETE) {
            subject = ADD_TITLE + subject;
          } else {
            subject = subject.replace(ADD_TITLE, '');
          }
          const updEvent = {
            subject: subject,
          };
          return garoon.api('/api/v1/schedule/events/' + eventId, 'PATCH', updEvent).then((reps) => {
            datastore.value.isAccepted = !datastore.value.isAccepted;
            return garoon.api('/api/v1/schedule/events/' + eventId + '/datastore/' + SCHEDULE_DATASTORE_KEY, 'PUT', datastore);
          }).then((resp) => {
            location.reload();
          });
        });
      }
    }
    return ev;
  });
})(jQuery.noConflict(true));

来訪予定ポートレット

  1. 次の HTML のサンプルコード をコピーして、HTML ポートレットを作成します。
    ポートレットを作成する手順は、 HTML ポートレットの設定 - ポートレットの作成 (External link) を参照してください。

  2. 次の JavaScript のサンプルコード をエディタにコピーして、ファイル名を「attendee-portlet.js」、文字コードを「UTF-8」で保存します。

  3. 作成したポートレットに、JavaScript / CSS によるカスタマイズを適用します。
    カスタマイズを適用する手順は、 ポータルのカスタマイズ (External link) を参照してください。

    適用するファイルおよびリンクは次のとおりです。

    • JavaScript カスタマイズ
      • jQuery:ダウンロードした jQuery.min.js
      • SweetAlert2: sweetalert2.min.js
      • Moment.js:ダウンロードした moment.min.jsmoment-with-locales.min.js
      • 受け付けユーザー定義:作成した attendee.js
      • 来訪予定ポートレット:作成した attendee-portlet.js
    • CSS カスタマイズ:
      • SweetAlert2:ダウンロードした sweetalert2.min.css
      • Garoon html/css/image-Kit for Customize:ダウンロードした grn_kit.css
      • css for SweetAlert on Garoon:ダウンロードした sweetalert_button_grn.css

    カスタマイズを適用した後の画面は、次のようになります。

  4. 作成したポートレットをポータルへ配置します。

    information

    ポートレットへのアクセスは、適宜アクセス権を設定してください。
    例:My ポートレットでの利用を許可しない、ポートレットを配置したポータルへのアクセスを受け付けユーザーのみに限定する、ポートレットへのアクセスを受け付けユーザーのみに限定する、など。

HTML のサンプルコード
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!--
* スケジュールで受付管理業務を実現する
* Copyright (c) 2018 Cybozu
*
* Licensed under the MIT License
* https://opensource.org/license/mit/
-->

<!-- 本日のスケジュール一覧表 -->
<div class="navi_viewchange_grn_kit">
  <div class="navi_viewchange_grn navi_viewchange_theme_grn">
    <div class="inline_block_grn">
      <ul class="button_group_ul_grn">
        <li class="button_group_li_grn">
          <button type="button" id="showNotOver" class="button_group_item_grn button_style_off_grn button_group_item_select_grn" aria-pressed="true">
            <span>未対応のみ表示</span>
          </button>
        </li>
        <li class="button_group_li_grn">
          <button type="button" id="showAll" class="button_group_item_grn button_style_off_grn " aria-pressed="false">
            <span> すべて表示 </span>
          </button>
        </li>
      </ul>
    </div>
  </div>
</div>
<table id="schedules" class="list_table_style1_grn_kit" style="width:100%;">
  <tbody>
    <tr>
      <th class="nowrap_grn_kit">時間</th>
      <th class="nowrap_grn_kit">予定</th>
      <th class="nowrap_grn_kit">施設</th>
      <th class="nowrap_grn_kit">登録者</th>
      <th class="nowrap_grn_kit">ボタン</th>
    </tr>
  </tbody>
</table>

<!-- コメント登録ダイアログ -->
<div id="commentDialog" style="display:none">
  <textarea id="comment" rows="3" cols="40"></textarea>    <!-- コメントへ反映する -->
  <div>
    <input type="checkbox" id="completed" checked="true"><label for="completed">対応を完了する</label>
  </div>
  <input type="hidden" id="eventId" />  <!-- 更新対象スケジュールのID -->
  <input type="hidden" id="subject" />  <!-- 更新前のタイトル -->
</div>
JavaScript のサンプルコード

ファイル名を「attendee-portlet.js」、文字コードを「UTF-8」で保存します。
ファイル名は任意ですが、ファイルの拡張子は「js」にしてください。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
/**
*
* スケジュールで受付管理業務を実現する
* Copyright (c) 2018 Cybozu
*
* Licensed under the MIT License
* https://opensource.org/license/mit/
*/

(function($) {
  'use strict';
  // 受付ユーザの予定として抽出対象とする予定メニュー
  const SCHEDULE_MENU = '来訪';
  // 受付時にタイトルに付加する文字列
  const ADD_TITLE = '【受付対応済み】';
  // 受付時に登録するコメント
  const INIT_COMMENT = 'お客様がお見えになりました。';
  /**
  * 共通SOAPコンテンツ
  * ${XXXX} の箇所は実施処理等に合わせて置換して使用
  */
  const SOAP_TEMPLATE =
        '<?xml version="1.0" encoding="UTF-8"?>' +
        '<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">' +
          '<soap:Header>' +
          '<Action>${ACTION}</Action>' +
          '<Timestamp>' +
            '<Created>${CREATED}</Created>' +
            '<Expires>2037-08-12T14:45:00Z</Expires>' +
          '</Timestamp>' +
          '<Locale>jp</Locale>' +
          '</soap:Header>' +
          '<soap:Body>' +
          '<${ACTION}>' +
            '${PARAMETERS}' +
          '</${ACTION}>' +
          '</soap:Body>' +
        '</soap:Envelope>';
  /**
  * コメント登録SOAPリクエストのparametersテンプレート
  * ${XXXX}の箇所は実施処理等に合わせて置換して使用
  */
  const COMMENT_PARAM_TEMPLATE =
            '<parameters>' +
            '<request_token>${REQUEST_TOKEN}</request_token>' +
            '<follow event_id="${EVENT_ID}" content="${COMMENT}"></follow>' +
            '</parameters>';
  // 予定一覧のデータ行テンプレート
  const LINE_TEMPLATE =
        '<tr class="list_table_linetwo_grn_kit ${STATUS_CLASS}">' +
          '<td><a target="_blank" href="/g/schedule/view.csp?event=${EVENT_ID}">${START} ~ ${END}</a></td>' +
          '<td>' +
              '<a target="_blank" href="/g/schedule/view.csp?event=${EVENT_ID}">' +
                  '<span class="subject">${SUBJECT}</span>' +
              '</a>' +
          '</td>' +
          '<td>${FACILITIES}</td>' +
          '<td><a target="_blank" href="/users/${CREATOR_CODE}">${CREATOR_NAME}</a></td>' +
          '<td>' +
              '${BUTTON}' +
              '<input type="hidden" name="eventId" value="${EVENT_ID}" />' +
              '<input type="hidden" name="subject" value="${SUBJECT}" />' +
          '</td>' +
        '</tr>';
  // 施設情報表示
  const FACILITY_LINK =
        '<a target="_blank" href="/g/schedule/facility_info.csp?faid=${FACILITY_ID}">' +
            '${FACILITY_NAME}' +
        '</a>';
  // 対応前ボタン
  const BUTTON_TEMPLATE = '<button class="completeButton button_main_grn_kit">対応連絡</button>';
  // 対応後ボタン(クリック不可)
  const COMPLETE_TEMPLATE = '<button class="button_normal_grn_kit" disabled>対応完了</button>';
  const SCHEDULE_DATASTORE_KEY = 'jp.co.cybozu.schedule.sample.attendee';
  // 文字列置換処理
  const replace = function(source, from, to) {
    return source.split(from).join(to);
  };
  // 文字列をHTMLエスケープ
  const escapeHtml = function(str) {
    return str
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;')
      .replace(/\n/g, '&#xA;');
  };
  // 受付ユーザの本日の予定を検索する
  const fetchTodayEvents = function() {
    // 本日の日付の取得
    const today = moment().format('YYYY-MM-DD');
    // 特定のユーザの本日の予定を検索する
    const params = {
      orderBy: 'start asc', // 開始時刻の昇順
      rangeStart: today + 'T00:00:00+09:00', // 本日0:00から23:59の指定で本日データを検索
      rangeEnd: today + 'T23:59:59+09:00', // ※ JSTのみ対応
      target: GAROON_SAMPLE_PROGRAM_ATTENDEE.id, // 受付ユーザはファイル管理に配置したグローバル変数で管理
      targetType: 'user'
    };
    return garoon.api('/api/v1/schedule/events', 'GET', params).then((resp) => {
      return resp.data.events;
    });
  };
  // カスタム項目(Schedule datastore)を取得する
  const fetchScheduleDataStore = function(eventId) {
    const url = '/api/v1/schedule/events/' + eventId + '/datastore/' + SCHEDULE_DATASTORE_KEY;
    return garoon.api(url, 'GET', {}).then((resp) => {
      return {
        id: eventId,
        datastore: resp.data
      };
    });
  };
  // 投稿するコメントを生成する
  const buildCommentRequest = function(token, eventId, comment) {
    let request = SOAP_TEMPLATE;
    request = replace(request, '${CREATED}', moment().add(-9, 'hours').format('YYYY-MM-DDTHH:mm:ssZ'));
    request = replace(request, '${ACTION}', 'ScheduleAddFollows');
    request = replace(request, '${PARAMETERS}', COMMENT_PARAM_TEMPLATE);
    request = replace(request, '${REQUEST_TOKEN}', token);
    request = replace(request, '${EVENT_ID}', eventId);
    request = replace(request, '${COMMENT}', escapeHtml(comment));
    return request;
  };
  // スケジュールにコメントを投稿する
  const postComment = function(eventId, comment) {
    // コメント登録のため、リクエストトークンを取得
    const token = garoon.base.request.getRequestToken();
    // コメントを追加(SOAP API)
    const request = buildCommentRequest(token, eventId, comment);
    // コメントを投稿する
    return $.ajax({
      type: 'post',
      url: '/g/cbpapi/schedule/api.csp',
      cache: false,
      async: false,
      data: request
    });
  };
  // 受付登録ダイアログを表示する
  const showDialog = function(eventId, subject, comment, clickedButton) {
    Swal.fire({
      title: 'コメント追加',
      html:
        '<textarea id="swal-comment" class="swal2-textarea" style="width: 80%; resize: vertical;">' + comment + '</textarea>' +
        '<div class="form-check">' +
        '  <input class="form-check-input" type="checkbox" id="swal-need-accept" checked>' +
        '  <label class="form-check-label" for="isAccept">対応を完了する</label>' +
        '</div>',
      preConfirm: function() {
        return {
          comment: document.getElementById('swal-comment').value,
          needAccept: document.getElementById('swal-need-accept').checked
        };
      },
      focusConfirm: false,
      showCancelButton: true,
      confirmButtonText: '登録',
      cancelButtonText: 'キャンセル'
    }).then((result) => {
      // キャンセルをクリックした場合、何もしない
      if (!result.value) {
        return;
      }
      // 「対応を完了する」にチェックがない場合、コメントを投稿して終わる
      if (!result.value.needAccept) {
        postComment(eventId, comment);
        return;
      }
      // 対応を完了する」にチェックした場合、受付対応済みにしてコメントを投稿する
      const event = {
        subject: ADD_TITLE + subject, // タイトルを更新
      };
      garoon.api('/api/v1/schedule/events/' + eventId, 'PATCH', event).then((resp) => {
        // 処理済みフラグを ON
        const datastore = {
          value: {
            isAccepted: true,
          },
        };
        return garoon.api('/api/v1/schedule/events/' + eventId + '/datastore/' + SCHEDULE_DATASTORE_KEY, 'PUT', datastore);
      }).then((_resp) => {
        // コメントを投稿する
        return postComment(eventId, comment);
      }).then((_resp) => {
        $(clickedButton).parent().parent().addClass('status_completed');
        $(clickedButton).parent().parent().find('.subject').html(ADD_TITLE + subject);
        $(clickedButton).parent().html(COMPLETE_TEMPLATE);
      });
    }).catch((err) => {
      console.error(err);
      Swal.fire({
        icon: 'error',
        title: '受付登録ができませんでした。',
        text: ''
      });
    });
  };

  /**
  * 受付ユーザの本日の来訪予定をポートレットに表示する
  * 画面読み込み完了時に行われる
  */
  $(document).ready(() => {
    let eventsForVisit;
    // 表示対象のイベントを検索する
    fetchTodayEvents().then((events) => {
      // 来訪でなければ表示しないため、絞り込みする
      // 予定の取得 API は予定メニューで絞り込めないため、予定を取得したあとに絞り込む
      eventsForVisit = events.filter((event) => {
        return event.eventMenu === SCHEDULE_MENU;
      });
      return garoon.Promise.all(eventsForVisit.map((event) => {
        return fetchScheduleDataStore(event.id);
      }));
    }).then((datastores) => {
      eventsForVisit.forEach((event) => {
        let line = LINE_TEMPLATE;
        const facilities = event.facilities.map((falicity) => {
          let facilityLink = FACILITY_LINK;
          facilityLink = replace(facilityLink, '${FACILITY_ID}', falicity.id);
          facilityLink = replace(facilityLink, '${FACILITY_NAME}', falicity.name);
          return facilityLink;
        });
        // 予定に対応するカスタム項目(Schedule datastore)を取得する
        const datastore = datastores.filter((v) => {
          return v.id === event.id;
        })[0].datastore;
        // 登録作業の実施有無によってボタンの表示を制御する
        // 行表示制御のため、クラスを設定する
        if (datastore.value.isAccepted === false) {
          line = replace(line, '${BUTTON}', BUTTON_TEMPLATE);
        } else {
          line = replace(line, '${BUTTON}', COMPLETE_TEMPLATE);
          line = replace(line, '${STATUS_CLASS}', 'status_completed');
        }
        // 開始終了は時分部分のみを取り出す
        line = replace(line, '${EVENT_ID}', event.id);
        line = replace(line, '${SUBJECT}', event.subject);
        line = replace(line, '${START}', moment(event.start.dateTime).format('HH:mm'));
        line = replace(line, '${END}', moment(event.end.dateTime).format('HH:mm'));
        line = replace(line, '${CREATOR_CODE}', event.creator.code);
        line = replace(line, '${CREATOR_NAME}', event.creator.name);
        line = replace(line, '${FACILITIES}', facilities.join('<br>'));
        $('#schedules tbody').append(line);
      });
      // 対応連絡ボタンのクリックイベントを追加する
      $(document).on('click', '.completeButton', function() {
        const eventId = $(this).parent().find('[name=eventId]').val();
        const subject = $(this).parent().find('[name=subject]').val();
        // 登録するコメントを初期化する
        const comment = INIT_COMMENT;
        // コメントを登録するダイアログを表示
        showDialog(eventId, subject, comment, $(this));
      });
      // すべて表示イベントを追加する
      $(document).on('click', '#showAll', function() {
        // 終了行を表示する
        $('.status_completed').each(function() {
          $(this).show();
        });
        $('#showNotOver').removeClass('button_group_item_select_grn');
        $('#showNotOver').attr('aria-pressed', 'false');
        $(this).addClass('button_group_item_select_grn');
        $(this).attr('aria-pressed', 'true');
      });
      // 未対応のみ表示イベントを追加する
      $(document).on('click', '#showNotOver', function() {
        // 終了行を隠す
        $('.status_completed').each(function() {
          $(this).hide();
        });
        $('#showAll').removeClass('button_group_item_select_grn');
        $('#showAll').attr('aria-pressed', 'false');
        $(this).attr('aria-pressed', 'true');
        $(this).addClass('button_group_item_select_grn');
      });
      // 初期表示は未対応のみ表示イベントを強制発火し、対応済みを隠す
      $('#showNotOver').click();
    });
  });
})(jQuery.noConflict(true));

解説

利用しているライブラリー

  • jQuery (External link) v3.6.0, ドキュメント (External link)
    • ボタンやクリックイベントの登録などに利用しています。
  • SweetAlert2 (External link) v11.0.16, ドキュメント (External link)
    • 来訪予定リストポートレットで、対応連絡を行う際のコメント入力ダイアログで使用しています。
  • Moment.js (External link) v2.29.1, ドキュメント (External link)
    • 来訪予定リストポートレットで、日時のフォーマット等、日付操作を簡略化するために使用しています。
  • Garoon html/css/image-Kit for Customize
    • Garoon ライクな UI で表示するためのライブラリーです。スケジュール参加者を自動設定する機能の「対応完了」ボタンや、来訪予定リストポートレットの表示で利用してます。
  • css for SweetAlert on Garoon (External link)
    • Garoon で SweetAlert2 を利用するとき、レイアウト崩れを防ぐ css です。

スケジュール参加者を自動設定する機能

サンプルコード 13 行目では、「受け付けユーザー」を追加する予定メニューを定義しています。
「来訪」以外の予定メニューで「受け付けユーザー」を追加したい場合、変更してください。

13
const ADD_MENU = '来訪';

15 行目では、受け付けを完了したときタイトルに付ける文言を定義しています。適宜変更してください。

15
const ADD_TITLE = '【受付対応済み】';

27 行目では、 予定の登録画面を表示した後のイベント 予定の変更画面を表示した後のイベント を使用しています。

27
garoon.events.on(['schedule.event.create.submit', 'schedule.event.edit.submit'], (ev) => {

30〜33 行目では、選択された予定メニューが「来訪」のとき、「受け付けユーザー」を予定の参加者に追加します。

30
31
32
33
// 予定メニューが「来訪」の場合
if (event.eventMenu === ADD_MENU) {
  // 受付ユーザを参加者に追加する
  event.attendees.push(GAROON_SAMPLE_PROGRAM_ATTENDEE);

このカスタマイズでは、スケジュールの受け付けの処理状況を、カスタム項目(Schedule datastore)で管理しています。
35〜45 行目では、 カスタム項目(Schedule datastore)の値を取得する API を使って、このスケジュールのカスタム項目(Schedule datastore)の値を取得します。
カスタム項目の値が取得できなければ、 カスタム項目(Schedule datastore)の値をセットする API を使って初期値を設定します。

35
36
37
38
39
40
41
42
43
44
45
let datastore = garoon.schedule.event.datastore.get(SCHEDULE_DATASTORE_KEY);
// 予定の登録時、または編集時に予定メニューを「来訪」に変更したとき
if (!datastore) {
  // カスタム項目(Schedule datastore)に初期値を設定
  datastore = {
    value: {
      isAccepted: false,
    },
  };
  garoon.schedule.event.datastore.set(SCHEDULE_DATASTORE_KEY, datastore);
}
information

カスタム項目(Schedule datastore)は、他サービスと API 連携して取得した値の保存などに利用できる項目で、画面には表示されません。
このカスタマイズでは、受け付け処理状況を保存するためにカスタマイズ項目を利用しています。

54 行目では、 予定の登録画面を表示した後のイベント を使用しています。
既存のスケジュールを再利用した場合、タイトルへの付加された【受け付け対応済み】の文言やカスタム項目での受け付け処理済みかのフラグが設定されたままなので、それらをクリアする必要があります。

54
garoon.events.on('schedule.event.create.show', (ev) => {

58~74 行目がクリアを行っている箇所です。
受け付け処理済みのスケジュールを再利用して新規スケジュールを作成した場合、カスタム項目(Schedule datastore)の内容も引き継がれ、受け付け処理済の状態で登録されてしまうため、それを防止しています。

58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// 再利用のスケジュールで、「来訪」かつ受付ユーザを参加者として含む来訪の予定の場合、
if (ev.reuse && event.eventMenu === ADD_MENU) {
  receptionUser = event.attendees.filter((attendee) => {
    return attendee.id === GAROON_SAMPLE_PROGRAM_ATTENDEE.id;
  });
  if (receptionUser) {
    // 【受付対応済み】をタイトルから削除
    event.subject = event.subject.replace(ADD_TITLE, '');
    // カスタム項目(Schedule datastore)を初期化
    const datastore = {
      value: {
        isAccepted: false,
      },
    };
    garoon.schedule.event.datastore.set(SCHEDULE_DATASTORE_KEY, datastore);
  }
}

来訪予定リストポートレット

13 行目では、リスト表示する予定メニューを定義しています。
「来訪」以外の予定を一覧に表示したい場合は変更してください。

12
13
// 受付ユーザの予定として抽出対象とする予定メニュー
const SCHEDULE_MENU = '来訪';

15 行目と 17 行目は、対応連絡を行った際に更新する内容の定義です。スケジュールのタイトルに 15 行目の文言を付加し、17 行目の文言をコメントとして登録します。

14
15
16
17
// 受付時にタイトルに付加する文字列
const ADD_TITLE = '【受付対応済み】';
// 受付時に登録するコメント
const INIT_COMMENT = 'お客様がお見えになりました。';

90~104 行目で、 複数の予定を取得する API を使って表示する予定を取得する処理です。
開始・終了日時が本日かつ、受け付けユーザーを参加者として含むスケジュールを取得します。

 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
// 受付ユーザの本日の予定を検索する
const fetchTodayEvents = function() {
  // 本日の日付の取得
  const today = moment().format('YYYY-MM-DD');
  // 特定のユーザの本日の予定を検索する
  const params = {
    orderBy: 'start asc', // 開始時刻の昇順
    rangeStart: today + 'T00:00:00+09:00', // 本日0:00から23:59の指定で本日データを検索
    rangeEnd: today + 'T23:59:59+09:00', // ※ JSTのみ対応
    target: GAROON_SAMPLE_PROGRAM_ATTENDEE.id, // 受付ユーザはファイル管理に配置したグローバル変数で管理
    targetType: 'user'
  };
  return garoon.api('/api/v1/schedule/events', 'GET', params).then((resp) => {
    return resp.data.events;
  });
};

106~114 行目が表示対象の 予定のカスタム項目(Schedule datastore)を取得する 処理です。
一覧に表示するスケジュールが受け付け処理済みかを取得しています。

105
106
107
108
109
110
111
112
113
114
// カスタム項目(Schedule datastore)を取得する
const fetchScheduleDataStore = function(eventId) {
  const url = '/api/v1/schedule/events/' + eventId + '/datastore/' + SCHEDULE_DATASTORE_KEY;
  return garoon.api(url, 'GET', {}).then((resp) => {
    return {
      id: eventId,
      datastore: resp.data
    };
  });
};

127〜140 行目は、スケジュールにコメントを投稿する処理です。
コメント登録 は SOAP API を利用するため、jQuery の ajax 関数を利用して HTTP リクエストを送信します。

126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
// スケジュールにコメントを投稿する
const postComment = function(eventId, comment) {
  // コメント登録のため、リクエストトークンを取得
  const token = garoon.base.request.getRequestToken();
  // コメントを追加(SOAP API)
  const request = buildCommentRequest(token, eventId, comment);
  // コメントを投稿する
  return $.ajax({
    type: 'post',
    url: '/g/cbpapi/schedule/api.csp',
    cache: false,
    async: false,
    data: request
  });
};

142 行目から、対応連絡登録するためのダイアログを作成しています。

141
142
  // 受付登録ダイアログを表示する
  const showDialog = function(eventId, subject, comment, clickedButton) {

167~190 行目が受け付け処理済み登録をする処理です。
受け付け対応済みの処理では、 予定を更新する API を使ってスケジュールのタイトルを更新し、 予定のカスタム項目(Schedule datastore)を更新する API を使って、受け付け処理済みを設定します。
これらの API は REST API なので、 Garoon REST API リクエストを送信する API を使って、HTTP リクエストを送信します。

141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
// 受付登録ダイアログを表示する
const showDialog = function(eventId, subject, comment, clickedButton) {
  Swal.fire({
    title: 'コメント追加',
    html:
        '<textarea id="swal-comment" class="swal2-textarea" style="width: 80%; resize: vertical;">' + comment + '</textarea>' +
        '<div class="form-check">' +
        '  <input class="form-check-input" type="checkbox" id="swal-need-accept" checked>' +
        '  <label class="form-check-label" for="isAccept">対応を完了する</label>' +
        '</div>',
    preConfirm: function() {
      return {
        comment: document.getElementById('swal-comment').value,
        needAccept: document.getElementById('swal-need-accept').checked
      };
    },
    focusConfirm: false,
    showCancelButton: true,
    confirmButtonText: '登録',
    cancelButtonText: 'キャンセル'
  }).then((result) => {
    // キャンセルをクリックした場合、何もしない
    if (!result.value) {
      return;
    }
    // 「対応を完了する」にチェックがない場合、コメントを投稿して終わる
    if (!result.value.needAccept) {
      postComment(eventId, comment);
      return;
    }
    // 対応を完了する」にチェックした場合、受付対応済みにしてコメントを投稿する
    const event = {
      subject: ADD_TITLE + subject, // タイトルを更新
    };
    garoon.api('/api/v1/schedule/events/' + eventId, 'PATCH', event).then((resp) => {
      // 処理済みフラグを ON
      const datastore = {
        value: {
          isAccepted: true,
        },
      };
      return garoon.api('/api/v1/schedule/events/' + eventId + '/datastore/' + SCHEDULE_DATASTORE_KEY, 'PUT', datastore);
    }).then((_resp) => {
      // コメントを投稿する
      return postComment(eventId, comment);
    }).then((_resp) => {
      $(clickedButton).parent().parent().addClass('status_completed');
      $(clickedButton).parent().parent().find('.subject').html(ADD_TITLE + subject);
      $(clickedButton).parent().html(COMPLETE_TEMPLATE);
    });
  }).catch((err) => {
    console.error(err);
    Swal.fire({
      icon: 'error',
      title: '受付登録ができませんでした。',
      text: ''
    });
  });
};

205 行目から、画面の読み込みが完了した際に発行するイベントを定義しています。来訪予定リストを表示します。

201
202
203
204
205
  /**
   * 受付ユーザの本日の来訪予定をポートレットに表示する
   * 画面読み込み完了時に行われる
   */
  $(document).ready(() => {

206~217 行目で、予定を取得とカスタム項目(Schedule datastore)を取得しています。
予定の取得 API は予定メニューで絞り込めないため、予定を取得したあとに絞り込みます。

206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
let eventsForVisit;
// 表示対象のイベントを検索する
fetchTodayEvents().then((events) => {
  // 来訪でなければ表示しないため、絞り込みする
  // 予定の取得 API は予定メニューで絞り込めないため、予定を取得したあとに絞り込む
  eventsForVisit = events.filter((event) => {
    return event.eventMenu === SCHEDULE_MENU;
  });
  return garoon.Promise.all(eventsForVisit.map((event) => {
    return fetchScheduleDataStore(event.id);
  }));
}).then((datastores) => {
  eventsForVisit.forEach((event) => {
    let line = LINE_TEMPLATE;
    const facilities = event.facilities.map((falicity) => {
      let facilityLink = FACILITY_LINK;
      facilityLink = replace(facilityLink, '${FACILITY_ID}', falicity.id);
      facilityLink = replace(facilityLink, '${FACILITY_NAME}', falicity.name);
      return facilityLink;
    });
    // 予定に対応するカスタム項目(Schedule datastore)を取得する
    const datastore = datastores.filter((v) => {
      return v.id === event.id;
    })[0].datastore;
    // 登録作業の実施有無によってボタンの表示を制御する
    // 行表示制御のため、クラスを設定する
    if (datastore.value.isAccepted === false) {
      line = replace(line, '${BUTTON}', BUTTON_TEMPLATE);
    } else {
      line = replace(line, '${BUTTON}', COMPLETE_TEMPLATE);
      line = replace(line, '${STATUS_CLASS}', 'status_completed');
    }
    // 開始終了は時分部分のみを取り出す
    line = replace(line, '${EVENT_ID}', event.id);
    line = replace(line, '${SUBJECT}', event.subject);
    line = replace(line, '${START}', moment(event.start.dateTime).format('HH:mm'));
    line = replace(line, '${END}', moment(event.end.dateTime).format('HH:mm'));
    line = replace(line, '${CREATOR_CODE}', event.creator.code);
    line = replace(line, '${CREATOR_NAME}', event.creator.name);
    line = replace(line, '${FACILITIES}', facilities.join('<br>'));
    $('#schedules tbody').append(line);
  });
  // 対応連絡ボタンのクリックイベントを追加する
  $(document).on('click', '.completeButton', function() {
    const eventId = $(this).parent().find('[name=eventId]').val();
    const subject = $(this).parent().find('[name=subject]').val();
    // 登録するコメントを初期化する
    const comment = INIT_COMMENT;
    // コメントを登録するダイアログを表示
    showDialog(eventId, subject, comment, $(this));
  });
  // すべて表示イベントを追加する
  $(document).on('click', '#showAll', function() {
    // 終了行を表示する
    $('.status_completed').each(function() {
      $(this).show();
    });
    $('#showNotOver').removeClass('button_group_item_select_grn');
    $('#showNotOver').attr('aria-pressed', 'false');
    $(this).addClass('button_group_item_select_grn');
    $(this).attr('aria-pressed', 'true');
  });
  // 未対応のみ表示イベントを追加する
  $(document).on('click', '#showNotOver', function() {
    // 終了行を隠す
    $('.status_completed').each(function() {
      $(this).hide();
    });
    $('#showAll').removeClass('button_group_item_select_grn');
    $('#showAll').attr('aria-pressed', 'false');
    $(this).attr('aria-pressed', 'true');
    $(this).addClass('button_group_item_select_grn');
  });
  // 初期表示は未対応のみ表示イベントを強制発火し、対応済みを隠す
  $('#showNotOver').click();
});

218~247 行目で、スケジュールをリストに出力します。

218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
eventsForVisit.forEach((event) => {
  let line = LINE_TEMPLATE;
  const facilities = event.facilities.map((falicity) => {
    let facilityLink = FACILITY_LINK;
    facilityLink = replace(facilityLink, '${FACILITY_ID}', falicity.id);
    facilityLink = replace(facilityLink, '${FACILITY_NAME}', falicity.name);
    return facilityLink;
  });
  // 予定に対応するカスタム項目(Schedule datastore)を取得する
  const datastore = datastores.filter((v) => {
    return v.id === event.id;
  })[0].datastore;
  // 登録作業の実施有無によってボタンの表示を制御する
  // 行表示制御のため、クラスを設定する
  if (datastore.value.isAccepted === false) {
    line = replace(line, '${BUTTON}', BUTTON_TEMPLATE);
  } else {
    line = replace(line, '${BUTTON}', COMPLETE_TEMPLATE);
    line = replace(line, '${STATUS_CLASS}', 'status_completed');
  }
  // 開始終了は時分部分のみを取り出す
  line = replace(line, '${EVENT_ID}', event.id);
  line = replace(line, '${SUBJECT}', event.subject);
  line = replace(line, '${START}', moment(event.start.dateTime).format('HH:mm'));
  line = replace(line, '${END}', moment(event.end.dateTime).format('HH:mm'));
  line = replace(line, '${CREATOR_CODE}', event.creator.code);
  line = replace(line, '${CREATOR_NAME}', event.creator.name);
  line = replace(line, '${FACILITIES}', facilities.join('<br>'));
  $('#schedules tbody').append(line);
});

258~267 行目は、「対応連絡」ボタンをクリックした際のイベント処理です。クリックされたスケジュールの ID、タイトルを元に、コメント登録ダイアログを表示します。

257
258
259
260
261
262
263
264
265
266
267
// すべて表示イベントを追加する
$(document).on('click', '#showAll', function() {
  // 終了行を表示する
  $('.status_completed').each(function() {
    $(this).show();
  });
  $('#showNotOver').removeClass('button_group_item_select_grn');
  $('#showNotOver').attr('aria-pressed', 'false');
  $(this).addClass('button_group_item_select_grn');
  $(this).attr('aria-pressed', 'true');
});

269~280 行目が表示対象のスケジュールを切り替えるためのイベント処理です。押されたボタンによって、その日のすべての来訪予定あるいは未処理の予定を表示します。

268
269
270
271
272
273
274
275
276
277
278
279
280
// 未対応のみ表示イベントを追加する
$(document).on('click', '#showNotOver', function() {
  // 終了行を隠す
  $('.status_completed').each(function() {
    $(this).hide();
  });
  $('#showAll').removeClass('button_group_item_select_grn');
  $('#showAll').attr('aria-pressed', 'false');
  $(this).attr('aria-pressed', 'true');
  $(this).addClass('button_group_item_select_grn');
});
// 初期表示は未対応のみ表示イベントを強制発火し、対応済みを隠す
$('#showNotOver').click();

おわりに

ぜひ、自社の受け付け業務への適用が可能か検討の上、活用してみてください。

API を利用したカスタマイズにより、Garoon の利便性はさらに向上できます。今後も Tips を定期的に公開していきますので、ご期待ください。

サンプルコードで使用している API

このカスタマイズでは、次の API を利用しています。
お使いの Garoon で使用できる API かどうかは、各 API のドキュメントを参照してください。

変更履歴

  • 2021 年 6 月 9 日
    • スケジュールの付加情報の保存先を、カスタム項目(additionalItems)からカスタム項目(datastore)に変更するなど、サンプルコードの修正をしました。
      この修正で、利用しているライブラリやリソースの適用先を変更しています。
      カスタム項目(additionalItems)を利用するカスタマイズを Garoon に適用している場合は、 リソースの準備 を参考に、カスタマイズを再適用してください。
information

この Tips は、2021 年 5 月版 Garoon で動作を確認しています。