【Garoon JavaScript API】旅費申請で入力補助を行う(予定の取得)

目次

概要

今回は Garoon のワークフローカスタマイズ第 3 弾として、スケジュールから外出の予定を取得して旅費申請を作成するサンプルをお届けします。

前提条件と注意事項

  • このカスタマイズには、クラウド版 Garoon またはパッケージ版 Garoon 4.6 以降の環境が必要です。
  • ワークフロー JavaScript カスタマイズは、JavaScript を適用してから申請された WF が対象になります。 それ以前に申請されたワークフローには適用されません。

できること

月末に該当月の旅費を一括申請する会社が多いかと思います。
営業部など外出の多い部署ですと、外出の日付と目的地などをひとつずつ入力するのはとてもたいへんです。
いつもスケジュールには外出の予定を登録しているのに、そのデータを引き継ぐことができるとたいへん便利になるのではないでしょうか。

今回は、その問題を解決できるカスタマイズサンプルを紹介します。

ここでは「 ワークフロー申請の作成画面が表示されたときのイベント」を利用します!

完成イメージ

例)スケジュールに登録されている予定(複数件)と、「旅費申請」というワークフローがあり、その項目の一部が同じ内容の場合
予定の一部項目の内容を「旅費申請」ワークフローに自動で入力するサンプルカスタマイズの完成イメージです。

  1. 旅費申請の編集画面を開くと、該当月が自動で入力され、[予定を取得する]ボタンが表示されます。
    該当月に月初 5 日までは先月の値が入力されます。
    例)6/4 に旅費申請の編集画面を開くと、該当月に 5 月が入力されます。
    下書きからの編集の場合はなにもしません。
  2. [予定を取得する]ボタンをクリックすると、外出予定を選択できるダイアログが表示されます。
    以下の条件を満たす予定のみ取得して表示します。
    • 通常予定
    • 予定メニューが往訪/【履歴】往訪/フェアのいずれか。
    • 公開予定
  3. 入力する予定をチェックして「取得する」ボタンをクリックすると、その下の行に予定の日付とタイトルが入力されます。
    チェックされている予定の数が行数を上回る場合は、ダイアログを表示してキャンセルします。
    予定のタイトルが文字数制限を超える場合は、制限文字数(20 文字)までカットする
    予定がなかった行は、日付を対象月の 1 日目で初期化する
    月の予定がない場合は、すべての行の日付を対象月の 1 日目で初期化する

詳細設定

予定メニューの設定と予定の登録

予定メニューを設定する

予定メニューに往訪/【履歴】往訪/フェアのいずれかを指定する必要があるので、まずは予定メニューを登録しましょう。

  1. 「システム管理(各アプリケーション)> スケジュール > 予定メニューの設定」の画面を表示します。
  2. [追加する]ボタンをクリックして往訪、【履歴】往訪、フェアを追加し、設定をクリックします。

予定を登録する

予定メニューの設定が完了しましたら、さっそく予定を登録しましょう。

  1. 画面右上の[アプリ一覧]からスケジュールをクリックして、スケジュール画面を表示します。
  2. [予定を登録する]ボタンから「予定の登録」画面を表示します。
  3. 「通常予定」タブで、次の項目を必ず指定してください。他の設定は任意です。
    • 日付:該当月の日付を指定します。
    • タイトル:ドロップダウンから先ほど登録した予定メニューのいずれかを選択します。
    • 公開方法:公開
  4. [登録する]をクリックします。

ワークフローの JavaScript/CSS によるカスタマイズを許可する設定

ワークフローの JavaScript/CSS によるカスタマイズは初期値では「許可しない」設定になっています。
そのため、まずはその設定を「許可する」に変更します。

  1. 「システム管理(各アプリケーション)> ワークフロー > 一般設定」の画面を表示します。
  2. 「一般設定」の「JavaScript / CSS によるカスタマイズの許可」項目を[許可する]を選択します。

  3. 設定変更後、[適用する]をクリックします。

Garoon ワークフローの申請フォームを作成する

ワークフローの項目の内容は、企業様によって異なります。
そのため、このサンプルの説明では、完成イメージで利用した旅費申請の申請フォームに JavaScript カスタマイズを充ていく流れを説明します。

まずは kintone でアプリを準備したのと同じく、Garoon で以下の項目を配置して、旅費申請ワークフローの申請フォームを作成していきます。
申請フォームの作成方法については 申請フォームの作成の流れ (External link) を参照してください。
フォーム作成は少し手間がかかるので、今回はそのまま読み込んで使えるサンプルフォームもご用意しています。(後述)

赤枠の中の項目は必ず設定してください。
「対象月」の下にある 1 行目 ~10 行目は同じ項目を設定して、各行の項目コードの最後の数値をゼロから 1 ずつ順番に増やします。
例、日付項目の項目コードの場合、1 行目を date_0、2 行目を date_1 とします。
ここでも項目コードは、JavaScript コード内でそれぞれの項目を指定するための一意の文字列なので、間違えないように設定してくださいね。

項目名 項目タイプ 項目コード 備考
対象月 メニュー taisho_tsuki メニュー項目に1月~12月を入力してください。
(JavaScriptカスタマイズ用項目) schedule_picker_space 「右隣への配置」をチェックする。
1. 日付 date_0
(JavaScriptカスタマイズ用項目) row_controller_0 「右隣への配置」をチェックする。
1. 文字列(1行) detail_0 「右隣への配置」をチェックする。
1. メニュー method_0 「右隣への配置」をチェックする。
メニュー項目に交通手段や宿泊などの項目を任意に入力する。
路線ナビ連携 路線ナビ連携 navi_0 「右隣への配置」をチェックする。

上記のとおり設定が完了したら、土台となる申請フォームの作成は完了です。

サンプルフォームのダウンロードについて

申請フォームを作成するのに少し時間がかかるので、まずは動きを見てみたいという方向けに、そのまま環境へ読み込んで使っていただけるサンプルの申請フォームを XML 形式でご用意しました。
完成イメージとおりの旅費申請の申請フォームになります。
以下から[sample_form.xml]リンクを右クリックして、コンテキストメニューから[リンク先を別名で保存]をクリックしてダウンロードしてください。

sample_form.xml

この XML ファイルを、「申請フォーム一覧」から読み込んでいただくと、「【サンプル】旅費申請(10 行)」という申請フォームが追加されます。
項目コードも設定済みの状態です。
サンプルフォームを追加する方法については、 XMLファイルから読み込む (External link) を参照してください。
サンプルフォームを利用する場合、専用経路として経路が読み込まれます。
必要に応じて経路を変更してください。

JavaScriptのファイルを適用する

許可設定が完了したらいよいよ JavaScript ファイルを申請フォームに適用していきます。

  1. JavaScript ファイルを保存します。
    次のサンプルコードをエディタにコピーして、文字コードを「UTF-8」にし、任意のファイル名で保存します。
    拡張子は「js」です。この記事では「workflow-expense.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
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    440
    441
    442
    443
    444
    445
    446
    447
    448
    449
    450
    451
    452
    453
    454
    455
    456
    457
    458
    459
    460
    461
    462
    463
    464
    465
    466
    467
    468
    469
    470
    471
    472
    473
    474
    475
    476
    477
    478
    479
    480
    481
    482
    483
    484
    485
    486
    487
    488
    489
    490
    491
    492
    493
    494
    495
    496
    497
    498
    499
    500
    
    /*
    * Garoon JavaScript API of sample program
    * Copyright (c) 2017 Cybozu
    *
    * Licensed under the MIT License
    * https://opensource.org/license/mit/
    */
    (($) => {
      'use strict';
    
      /** @const {Number} */ const NUMBER_OF_ROWS = 10; // 入力行数
      /** @const {Number} */ const LIMIT_OF_TITLESTRING = 20; // タイトルの制限文字数
      let year = '';
      let month = '';
    
      /**
       * 文字列をエスケープする
      * @param  {string} str
      * @return {string}
      */
      const escapeStr = (str) => {
        if (!str) {
          return '';
        }
        return str
          .replace(/&/g, '&')
          .replace(/</g, '&lt;')
          .replace(/>/g, '&gt;')
          .replace(/"/g, '&quot;')
          .replace(/'/g, '&#39;');
      };
    
      /**
       * 文字列の長さを制限する
      * @param  {string} str
      * @return {string}
      */
      const trimStr = (str) => {
        if (str.length > LIMIT_OF_TITLESTRING) {
          return str.slice(0, LIMIT_OF_TITLESTRING);
        }
        return str;
      };
    
      /**
       * ガルーンAPI実行用のリクエストヘッダを作成する
      * @param  {string} services
      * @param  {string} action
      * @return {string}
      */
      const makeXMLHeader = (services, action) => {
        let xmlns;
        switch (services) {
          case 'base':
            xmlns = 'base_services="http://wsdl.cybozu.co.jp/base/2008"';
            break;
          case 'bulletin':
            xmlns = 'workflow_services="http://wsdl.cybozu.co.jp/bulletin/2008"';
            break;
          default:
            alert('Can not select services');
            return undefined;
        }
    
        const xmlHeader =
          '<?xml version="1.0" encoding="UTF-8"?>' +
          '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope" ' +
          'xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' +
          `xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:${xmlns}>` +
          '<SOAP-ENV:Header>' +
          '<Action SOAP-ENV:mustUnderstand="1" ' +
          `xmlns="http://schemas.xmlsoap.org/ws/2003/03/addressing">${escapeStr(action)}</Action>` +
          '<Timestamp SOAP-ENV:mustUnderstand="1" Id="id" ' +
          'xmlns="http://schemas.xmlsoap.org/ws/2002/07/utility">' +
          '<Created>2037-08-12T14:45:00Z</Created>' +
          '<Expires>2037-08-12T14:45:00Z</Expires>' +
          '</Timestamp>' +
          '<Locale>jp</Locale>' +
          '</SOAP-ENV:Header>';
        return xmlHeader;
      };
    
      /**
       * チェックされた予定のオブジェクトを作成する
      * @param  {object} checkedRows
      * @return {object}
      */
      const toEventsObj = ($checkedRows) => {
        const events = [];
        $checkedRows.each((idx, row) => {
          events.push({
            date: $(row).children('td:nth-child(2)').text(),
            title: $(row).children('td:nth-child(3)').text()
          });
        });
        return events;
      };
      /**
       * リクエストの日付を初期化する
      * @param  {Object} request
      * @param  {string} year
      * @param  {string} month
      * @return {Object}
      */
      const initDate = (request) => {
        const firstDate = `${year}-${('0' + month).slice(-2)}-01`;
        for (let di = 0; di < NUMBER_OF_ROWS; di++) {
          request.items[`date_${di}`].value = firstDate;
        }
      };
    
      /**
       * 取得したスケジュールをセットする
      * @param  {Object} events schedule events
      * @param  {Object} request workflow request
      */
      const setEvents = (events, request) => {
        // //日付順でソート
        events.sort((a, b) => {
          return luxon.DateTime.fromISO(a.date).toSeconds() - luxon.DateTime.fromISO(b.date).toSeconds();
        });
    
        let num = 0;
        events.forEach((val, idx) => {
          request.items[`date_${num}`].value = luxon.DateTime.fromISO(val.date).toFormat('yyyy-MM-dd');
          request.items[`detail_${num}`].value = trimStr(val.title);
          num++;
        });
        garoon.workflow.request.set(request);
      };
    
      /**
       * 予定リストにcssを追加する
      */
      const addCssToDialog = () => {
        $('#grn_schedule_list').css('height', '300px');
        $('#grn_schedule_list').css('overflow', 'auto');
    
        // オンマウス:マウスが乗っている一行の色を変える
        // オンクリック:チェックを入れる/消す、「選択中の予定」の数を変更する
        $('tr.event_list_row').css('cursor', 'pointer');
        $('tr.event_list_row').on({
          mouseenter: function() {
            $(this).css('background-color', '#EEEEEE');
          },
          mouseleave: function() {
            $(this).css('background-color', '');
          },
          click: function() {
            const $c = $(this).children('td').children('input[class=event_list_check]');
            const $num = $('#checked_schedules_number');
            if ($c.prop('checked')) {
              $c.prop('checked', '');
              $num.text(Number($num.text()) - 1);
            } else {
              $c.prop('checked', 'checked');
              $num.text(Number($num.text()) + 1);
            }
          }
        });
    
        $('input[class=event_list_check]').css('cursor', 'pointer');
        $('input[class=event_list_check]').on({
          click: function() {
            if ($(this).prop('checked')) {
              $(this).prop('checked', '');
            } else {
              $(this).prop('checked', 'checked');
            }
          }
        });
      };
      /**
       * 予定リストのcssを削除する
      */
      const removeCss = () => {
        $('.swal2-content').css('height', '');
        $('.swal2-content').css('overflow', '');
      };
    
      /**
       * モーダルで表示する予定リストのHTMLを作成する。予定がない場合はfalseを返す
      * @param  {object} $events
      * @return {string}
      */
      const eventListHtmlIfExist = ($events) => {
        let $event;
        let num = 0;
        const data = {events: []};
    
        // イベントのリストを作成する
        $events.each((idx, val) => {
          $event = $(val);
    
          // 通常公開予定、予定メニュー「往訪」「フェア」以外のデータは無視する
          if ($event.attr('event_type') !== 'normal') {
            return true;
          }
          if ($event.attr('public_type') !== 'public') {
            return true;
          }
          if ($event.attr('plan') !== '往訪' &&
                    $event.attr('plan') !== '【履歴】往訪' &&
                    $event.attr('plan') !== 'フェア') {
            return true;
          }
    
          // 予定の日時データを抽出する。時刻が登録されていない予定の場合、日付データのみを抽出する
          let eventDate;
          if ($event.find('datetime').attr('start') !== undefined) {
            eventDate = $event.find('datetime').attr('start');
          } else {
            eventDate = $event.find('date').attr('start');
          }
    
          data.events.push({
            date: luxon.DateTime.fromISO(eventDate),
            detail: $event.attr('detail')
          });
    
          num++;
          return true;
        });
    
        // 対象イベントが無い場合はfalseを返す
        if (num === 0) {
          return false;
        }
    
        // 日付順に並び替える
        data.events.sort((a, b) => {
          return a.date.toSeconds() - b.date.toSeconds();
          // return a.date.unix() - b.date.unix();
        });
        // 表示形式を揃える
        data.events.forEach((val, idx) => {
          // val.date = val.date.format('YYYY-MM-DD');
          val.date = val.date.toFormat('yyyy-MM-dd');
        });
        data.number = data.events.length;
        data.maxNumber = NUMBER_OF_ROWS;
    
        // 対象イベントのhtmlを作成する
        const html = [
          '<div id="grn_schedules_number"><strong>選択されている予定の数:',
          '<span id="checked_schedules_number">{{>number}}</span></strong>',
          ' 入力可能な予定の数:{{>maxNumber}}</div>',
          '<div id="grn_schedule_list">',
          '<table class="schedule_events_selector" style="width:100%;">',
          '<thead><tr><th></th><th>日付</th><th>タイトル</th></tr></thead>',
          '<tbody>',
          '{{for events}}',
          '<tr class="event_list_row">',
          '<td><input type="checkbox" checked="checked" class="event_list_check"></td>',
          '<td>{{>date}}</td>',
          '<td>{{>detail}}</td>',
          '</tr>',
          '{{/for}}',
          '</tbody></table>',
          '</div>'
        ].join('');
        const template = $.templates(html);
    
        return template(data);
      };
      /**
       * 予定選択ダイアログを表示する。予定がない場合は初期化ダイアログを表示する。
      * @param  {object} $events
      * @param  {object} request
      */
      const showSelectorDialog = ($events, request) => {
        // 表示するHTMLを作成する。対象の予定がない場合はfalse
        const html = eventListHtmlIfExist($events);
    
        // 対象の予定がない場合
        if (!html) {
          return Swal.fire({
            type: 'info',
            title: request.items.taisho_tsuki.value + '1日で日付を初期化しますか?',
            text: '対象月に往訪/フェアの予定がありませんでした',
            showCloseButton: true,
            showCancelButton: true,
            confirmButtonText: '初期化する',
            cancelButtonText: 'キャンセル',
            confirmButtonColor: '#64b2ed',
            customClass: {
              confirmButton: 'swal2-button__adjust',
              cancelButton: 'swal2-button__adjust'
            }
          }).then((result) => {
            if (!result.value) {
              return;
            }
            // 日付を初期化する
            initDate(request);
            garoon.workflow.request.set(request);
          }).catch((e) => {
            removeCss();
          });
    
        }
    
        // 予定が存在しHTMLを作成できた場合
        Swal.fire({
          type: 'info',
          title: '以下の予定を取得します',
          showCloseButton: true,
          showCancelButton: true,
          confirmButtonText: '取得する',
          cancelButtonText: 'キャンセル',
          confirmButtonColor: '#64b2ed',
          customClass: {
            confirmButton: 'swal2-button__adjust',
            cancelButton: 'swal2-button__adjust'
          },
          width: 600,
          html: html,
          preConfirm: function() {
            return new Promise((resolve) => {
              const $checkedRows = $('input[class=event_list_check]:checked').parents('tr');
              if ($checkedRows.length > NUMBER_OF_ROWS) {
                const message = '取得対象の予定が' + NUMBER_OF_ROWS + '件を超えています。\n' +
                                '対象を減らすか、ワークフローを分けて申請してください。';
                throw new Error(message);
              }
              resolve();
            }).catch((e) => {
              Swal.showValidationMessage(e.message);
            });
          }
        }).then((result) => {
          if (!result.value) {
            return;
          }
          const $checkedRows = $('input[class=event_list_check]:checked').parents('tr');
          const events = toEventsObj($checkedRows);
          // 選択されている予定がないとき
          if (events.length === 0) {
            Swal.fire({
              type: 'info',
              title: request.items.taisho_tsuki.value + '1日で日付を初期化しますか?',
              text: '予定が選択されませんでした',
              showCloseButton: true,
              showCancelButton: true,
              confirmButtonText: '初期化する',
              cancelButtonText: 'キャンセル',
              confirmButtonColor: '#64b2ed',
              customClass: {
                confirmButton: 'swal2-button__adjust',
                cancelButton: 'swal2-button__adjust'
              }
            }).then((result2) => {
              if (result2.value) {
                // 日付を初期化する
                initDate(request);
                garoon.workflow.request.set(request);
              }
            });
          }
    
          // 予定をセットする
          initDate(request);
          setEvents(events, request);
        }).finally(() => {
          // htmlに当てたcssを取り除く
          removeCss();
        });
    
        // 予定リストにcssを追加する
        addCssToDialog();
        return true;
      };
    
      /**
       * 該当月のスケジュールを取得する
      * @return {xml}
      */
      const getSchedulesOfTheMonth = () => {
        return new Promise((resolve, reject) => {
          const xhr = new XMLHttpRequest();
          const url = '/g/cbpapi/schedule/api.csp';
          const apiName = 'ScheduleGetEvents';
          const firstDate = year + '-' + ('0' + month).slice(-2) + '-01';
          const startTime = luxon.DateTime.fromISO(firstDate).startOf('day').toUTC().toString();
          const endTime = luxon.DateTime.fromISO(firstDate).endOf('month').endOf('day').toUTC().toString();
          const followRequest = [
            makeXMLHeader('bulletin', apiName),
            '<SOAP-ENV:Body>',
            `<${escapeStr(apiName)}>`,
            `<parameters xmlns="" start="${escapeStr(startTime)}`,
            `" end="${escapeStr(endTime)}"></parameters>`,
            `</${escapeStr(apiName)}>`,
            '</SOAP-ENV:Body>',
            '</SOAP-ENV:Envelope>'
          ].join('');
    
          xhr.open('POST', url, true);
          xhr.onload = function() {
            if (xhr.readyState === 4 && xhr.status === 200) {
              resolve(xhr.responseXML);
            }
          };
          xhr.send(followRequest);
        });
      };
    
      /**
          * 日付の表示に使う対象年度と対象月を設定する
          * @param {object} request
          */
      const setYearAndMonth = (request) => {
        // 対象月
        month = request.items.taisho_tsuki.value.slice(0, -1) || '';
        if (!month) {
          return;
        }
    
        // 対象年度
        const today = luxon.DateTime.now();
        // 1月に昨年12月の予定を取得する場合は、前年度の値を入力する
        if (month === '12' && today.month === 1) {
          year = String(today.minus({years: 1}).year);
        } else {
          year = String(today.year);
        }
      };
    
      /**
       * スケジュール取得ボタンが押されたときの挙動
      */
      const onclickGetScheduleButton = () => {
        const request = garoon.workflow.request.get();
        if (!request.items.taisho_tsuki.value) {
          return;
        }
    
        setYearAndMonth(request);
    
        // スケジュールを取得
        getSchedulesOfTheMonth().then((resp) => {
    
          const $events = $(resp).find('schedule_event');
    
          // 予定選択ダイアログを表示
          showSelectorDialog($events, request);
        }).catch((error) => {
          alert('error:' + error.message);
        });
      };
    
      /**
       * スケジュール取得ボタンを追加する
      * @param {object} event
      */
      const addGetScheduleButton = (event) => {
        const space = garoon.workflow.request.getSpaceElement('schedule_picker_space');
        const button = document.createElement('input');
        button.setAttribute('type', 'button');
        button.setAttribute('id', 'get_schedules_button');
        button.setAttribute('value', '予定を取得する');
        space.appendChild(button);
        const getButton = document.getElementById('get_schedules_button');
        getButton.onclick = function() {
          onclickGetScheduleButton(event);
        };
      };
    
      /**
       * 該当月をeventに入力する
      * @param  {object} event
      * @return {object}
      */
      const setMonth = (event) => {
        const items = event.request.items;
        // すでに対象月の値が存在する場合は抜ける
        if (items.taisho_tsuki.value) {
          return;
        }
        const today = luxon.DateTime.now();
        if (today.day <= 5) {
          // 毎月5日までは、先月の値を入力するluxon.DateTime.now().minus({months: 1}).month;
          items.taisho_tsuki.value = `${String(today.minus({months: 1}).month)}月`;
        } else {
          items.taisho_tsuki.value = `${String(today.month)}月`;
        }
      };
    
      // 申請作成時のイベント
      garoon.events.on('workflow.request.create.show', (event) => {
        // 下書き以外の場合は対象月フィールドを入力する
        if (!event.draft) {
          setMonth(event);
        }
    
        // ボタンを追加する
        addGetScheduleButton(event);
        return event;
      });
    
    })(jQuery.noConflict(true));
  2. CSS ファイルを保存します。
    次のサンプルコードをエディタにコピーして、文字コードを「UTF-8」にし、任意のファイル名で保存します。
    拡張子は「css」です。この記事では「workflow-expense.css」としています。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    /*
    * Garoon JavaScript API of sample program
    * Copyright (c) 2017 Cybozu
    *
    * Licensed under the MIT License
    */
    .swal2-button__adjust {
      height: auto;
    }
  3. 「申請フォーム情報」部分の右端にある「JavaScript / CSS によるカスタマイズ」をクリックします。

  4. 「カスタマイズ」項目に「適用する」を選択し、[JavaScript カスタマイズ]と[CSS カスタマイズ]に、1. と 2. で保存した JavaScript ファイル、css ファイル、および以下のライブラリを指定して「設定する」をクリックします。
    このカスタマイズでは、 Cybozu CDN の次のライブラリを使用します。

    • jQuery
      • https://js.cybozu.com/jquery/3.6.4/jquery.min.js
    • JSRender
      • https://js.cybozu.com/jsrender/1.0.12/jsrender.min.js
    • Luxon
      • https://js.cybozu.com/luxon/3.3.0/luxon.min.js
    • SweetAlert2
      • https://js.cybozu.com/sweetalert2/v11.7.3/sweetalert2.min.js
      • https://js.cybozu.com/sweetalert2/v11.7.3/sweetalert2.min.css
    • Font Awesome
      • https://js.cybozu.com/font-awesome/v6.4.0/js/all.min.js
      • https://js.cybozu.com/font-awesome/v6.4.0/css/fontawesome.min.css

以上ですべての設定は完了です!お疲れさまでした!最初にお見せした完成イメージのとおり、動けば成功です。

jQuery利用時の注意事項

jQuery を使った書き方をする際には、製品内で使っている jQuery と競合しないように次の書き方をおすすめします。

1
2
3
4
(($) => {
  // バージョン確認
  console.log($().jquery);
})(jQuery.noConflict(true));

おわりに

Garoon JavaScript API のカスタマイズサンプル第三弾、スケジュールとワークフローとの連携方法を紹介しました。面倒なコピー&ペースト作業がなくなり、月末の旅費申請はより楽になると思います。
ぜひ試してみてください。
他にも便利な使い方がありますので、その紹介はまた次の機会に。
お楽しみに!

変更履歴

  • 2019/11/12
    SweetAlert2 のバージョンを v6.4.2 → v8.18.6 に変更し、ソースコードを修正しました。
    その他ボタンのレイアウト崩れ対策のため、workflow-expense.css を追加しました。
  • 2023/04/05
    moment.js を Luxon に変更しました。
    JavaScript のコードを ES6 に対応させました。