KintoneとOutlookカレンダーを連携して工数を集計する

# やりたいこと
スケジュールをOutlookカレンダーで行っているので、そのデータを元に工数を集計できるようにしたい。

## 仕様
一覧画面に集計開始日と終了日を日付形式で設定し、その期間のOutlookカレンダーのデータを取得して一覧にする。
既に登録済みのデータは更新する。

## ソースはこちら
- [GitHub](https://github.com/soramoyou04/kintone-connect-outlook-schedule)

## 公式にサンプルがあるのでこれをベースにする
[Outlook連携 - kintoneからOutlookスケジュールを登録しよう！！](https://developer.cybozu.io/hc/ja/articles/360022249111-Outlook%E9%80%A3%E6%90%BA-kintone%E3%81%8B%E3%82%89Outlook%E3%82%B9%E3%82%B1%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%82%92%E7%99%BB%E9%8C%B2%E3%81%97%E3%82%88%E3%81%86-)

公式サンプルでは予定の登録だけなので、これを改造して予定を取得をできるようにする

### JS/CSS
サンプルからの変更としては [`dateformat.js`](http://www.enjoyxstudy.com/javascript/dateformat/) を追加しています。

- JS
  - https://code.jquery.com/jquery-3.5.1.min.js
  - dateformat.js
  - https://secure.aadcdn.microsoftonline-p.com/lib/1.0.0/js/msal.js
  - kintone-ui-comopnent.min.js
  - kintone-js-sdk.min.js
  - https://js.cybozu.com/sweetalert2/v8.17.6/sweetalert2.min.js
  - common-js-functions.min.js
  - kintone-connect-outlook-schedule-common.js
  - oauth.js
  - kintone-connect-outlook-schedule.js
  - https://kintone.github.io/kintoneUtility/kintoneUtility.min.js
- CSS
  - kintone-ui-component.min.css
  - https://js.cybozu.com/sweetalert2/v8.17.6/sweetalert2.min.css

### フォーム設定

| 項目 | フィールド形式 | フィールドコード |
|:--|:--|:--|
| イベントID | 文字列（1行） | EventId |
| 更新キー | 文字列（1行） | UpdateKey |
| 件名 | 文字列（1行） | To_Do |
| 担当者 | ユーザー選択 | Assignees |
| 開始日 | 日時 | From |
| 終了日 | 日時 | To |
| 工数（時） | 数値 | WorkTime |
| 終日 | チェックボックス | AllDay |
| 詳細内容| 文字列（複数行） | Details |
| 添付ファイル | 添付ファイル | Attachments |

### ヘッダー部をカスタマイズ
一覧画面のヘッダー部に「取得開始日」、「取得終了日」、「実行ボタン」配置の設定をする

#### 言語情報にボタン表記名を追加

```kintone-connect-outlook-schedule.js
lang: (
  en: {
    getEvent: 'Get Event',
  },
  ja: {
    getEvent: '予定を取得',
  }

settings: (
  ui: {
    buttons: {
      getEvent: {
        text: 'getEvent',
        type: 'normal'
      }
    }
  }
}
```

#### 日付項目とボタンのスタイルを設定

```kintone-connect-outlook-schedule.js
uiCreateForIndex: function(kintoneHeaderSpace) {
~
  // ラベルを設定
  this.data.ui.btnGetEvent = this.createButton(this.setting.ui.buttons.getEvent, this.setting.i18n.button);
  this.data.ui.labelFromDate = new kintoneUIComponent.Label({text: '取得開始日'});
  this.data.ui.labelToDate = new kintoneUIComponent.Label({text: '取得終了日'});

  // スタイルを設定
  this.data.ui.btnGetEvent.element.style.display = 'inline-block';
  this.data.ui.btnGetEvent.element.style.margin = '0 15px 15px 15px';
  this.data.ui.labelFromDate.element.style.display = 'inline-block';
  this.data.ui.labelFromDate.element.style.margin = '0 15px 15px 15px';
  this.data.ui.labelToDate.element.style.display = 'inline-block';
  this.data.ui.labelToDate.element.style.margin = '0 15px 15px 15px';
  
  // 日付の初期値を入力
  var iFrom = storage.inputFromDate ? new Date(storage.inputFromDate) : new Date();
  var iTo = storage.inputToDate ? new Date(storage.inputToDate) : new Date();
  this.data.ui.inputFromDate = new kintoneUIComponent.DateTime({value: iFrom, type: 'date', locale: 'ja', dateFormat: 'YYYY/MM/dd'});
  this.data.ui.inputToDate = new kintoneUIComponent.DateTime({value: iTo, type: 'date', locale: 'ja', dateFormat: 'YYYY/MM/dd'});

  // 各要素を配置
  this.data.ui.kintoneCustomizeOutlookHeaderSigned.appendChild(this.data.ui.labelFromDate.render());
  this.data.ui.kintoneCustomizeOutlookHeaderSigned.appendChild(this.data.ui.inputFromDate.render());
  this.data.ui.kintoneCustomizeOutlookHeaderSigned.appendChild(this.data.ui.labelToDate.render());
  this.data.ui.kintoneCustomizeOutlookHeaderSigned.appendChild(this.data.ui.inputToDate.render());
  this.data.ui.kintoneCustomizeOutlookHeaderSigned.appendChild(this.data.ui.btnGetEvent.render());
}
```

#### ボタン押下時のイベント登録する

```kintone-connect-outlook-schedule.js
kintone.events.on('app.record.index.show', function(event) {
~
  // 予定取得ボタン押下時
  kintoneScheduleService.data.ui.btnGetEvent.on('click', function() {
    outlookAPI.getEvent();
  });
}
```

### APIリクエストを設定する
calendarviewを使うので追加しましょう。
こんな感じで変更しました。

```kintone-connect-outlook-schedule-common.js
  graphApiScorp: {
    scopes: [
      'calendars.readwrite',
      'calendars.read'
    ],
  },

  event: {
    eventUrl: 'https://graph.microsoft.com/v1.0/me/events',
    eventGetUrl: 'https://graph.microsoft.com/v1.0/me/calendarview'
  },

  kintone: {
    fieldCode: {
      // 件名
      subject: 'To_Do',
      // 詳細内容
      body: 'Details',
      // 開始日時
      startDate: 'From',
      // 終了日時
      endDate: 'To',
      // イベントID
      eventId: 'EventId',
      // 添付ファイル
      attachFile: 'Attachments',
      // 更新キー
      updateKey: 'UpdateKey',
      // 終日
      isAllDay: 'AllDay',
      // ユーザー指定
      assignees: 'Assignees',
      // 工数
      worktime: 'WorkTime',
    }
  }
```

### ボタン押下時の処理を追加する

宣言部で必要な要素を取得する

```kintone-connect-outlook-schedule.js
  var SUBJECT_FIELD_CODE = KAC.kintone.fieldCode.subject;
  var BODY_FIELD_CODE = KAC.kintone.fieldCode.body;
  var START_DATE_FIELD_CODE = KAC.kintone.fieldCode.startDate;
  var END_DATE_FIELD_CODE = KAC.kintone.fieldCode.endDate;
  var ATTACH_FILE_FIELD_CODE = KAC.kintone.fieldCode.attachFile;
  var EVENT_ID_FIELD_CODE = KAC.kintone.fieldCode.eventId;
  var EVENT_URL = KAC.event.eventUrl;
  var UPDATE_KEY_FIELD_CODE = KAC.kintone.fieldCode.updateKey;
  var ALLDAY_FIELD_CODE = KAC.kintone.fieldCode.isAllDay;
  var ASSIGNEES_FIELD_CODE = KAC.kintone.fieldCode.assignees;
  var WORKTIME_FIELD_CODE = KAC.kintone.fieldCode.worktime;
  var EVENT_GET_URL = KAC.event.eventGetUrl;
```

#### 予定表からデータを取得する

```kintone-connect-outlook-schedule.js
var outlookAPI = {
~
// outlook予定表取得
getEvent: function() {
  var self = this;
  var accessToken;
  var header;
  KC.ui.loading.show();

  // アクセストークンを取得
  if (kintoneScheduleService.isExpireAccessToken()) {
    accessToken = storage.getItem('SESSION_KEY_TO_ACCESS_TOKEN');
  } else {
    KC.ui.loading.hide();
    return;
  }

  // タイムゾーンを日本設定
  header = {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json',
    'Prefer': 'outlook.timezone=\"Tokyo Standard Time\"',
    'outlook.body-content-type': 'text'
  };

  // ヘッダーの日付情報を取得してリクエストURLを作成
  var dateFormat = new DateFormat("yyyy/MM/dd");
  var fromDate = dateFormat.format(new Date(kintoneScheduleService.data.ui.inputFromDate.getValue()));
  var toDate = dateFormat.format(new Date(kintoneScheduleService.data.ui.inputToDate.getValue()));
  var eventGetUrl = EVENT_GET_URL + "?StartDatetime=" + fromDate + "T00:00:00" + "&EndDatetime=" + toDate + "T23:59:59";

  // 入力内容を保持（再表示用）
  storage.inputFromDate = kintoneScheduleService.data.ui.inputFromDate.getValue();
  storage.inputToDate = kintoneScheduleService.data.ui.inputToDate.getValue();

  // Outlookの予定表取得
  kintone.proxy(eventGetUrl, 'GET', header, {}).then(function(res) {
    var data = JSON.parse(res[0]).value;
    if (data === undefined) {
      Swal.fire({
        title: 'ERROR!',
        type: 'error',
        text: kintoneScheduleService.setting.i18n.message.error.accessOutlookFailure,
        allowOutsideClick: false
      });
      KC.ui.loading.hide();
      return;
    }

    // データがない場合
    if (data.length === 0) {
      Swal.fire({
        title: 'WARN!',
        type: 'warning',
        text: kintoneScheduleService.setting.i18n.message.warning.noMail,
        allowOutsideClick: false
      });
      KC.ui.loading.hide();
      return;
    }

    // 取得したスケジュールを登録
    self.putScheduleToKintoneApp(0, data, accessToken).catch(function(err) {
      Swal.fire({
        title: 'ERROR!',
        type: 'error',
        text: kintoneScheduleService.setting.i18n.message.error.addKintoneRecordFailure,
        allowOutsideClick: false
      });
      KC.ui.loading.hide();
    });
  }, function(err) {
    Swal.fire({
      title: 'ERROR!',
      type: 'error',
      text: kintoneScheduleService.setting.i18n.message.error.getOutlookMailFailure,
      allowOutsideClick: false
    });
    KC.ui.loading.hide();
  });
},
~
};
```

#### 取得したデータのチェック
新規なら登録処理、登録済みなら更新処理に振り分ける

```kintone-connect-outlook-schedule.js
var outlookAPI = {
~
// 取得した予定をkintoneへ登録
putScheduleToKintoneApp: function(index, data, accessToken) {
  var self = this;
  // キントーンに登録済みのデータかをチェックする
  return this.getScheduleIDIndexIsNotRetrived(index, data).then(function(indexSchedule) {
    // 登録済の予定なら更新
    if (indexSchedule === null) {
      return self.updateScheduleIntoKintone(data[index], accessToken).then(function(result) {
        // 次のデータ
        if (index + 1 < data.length) {
          return self.putScheduleToKintoneApp(index + 1, data, accessToken);
        }
        return null;
      });
    }

    // 未登録の予定なら登録
    return self.addScheduleIntoKintone(data[indexSchedule], accessToken).then(function(result) {
      // 次のデータ
      if (indexSchedule + 1 < data.length) {
        return self.putScheduleToKintoneApp(indexSchedule + 1, data, accessToken);
      }
      return null;
    });
  }).then(function(resp) {
    window.location.reload();
    KC.ui.loading.hide();
  });
},

getScheduleIDIndexIsNotRetrived: function(index, data) {
  var self = this;
  // 取得済み予定かチェック
  return self.checkSchedulesIsNotExistsOnkintone(data[index].id.substr(data[index].id.length-64)).then(function(resp) {
    if (resp === false) {
      if (data.length <= index + 1) {
        return null;
      }
      return self.getScheduleIDIndexIsNotRetrived(index + 1, data);
    }
    return index;
  });
},

// 取得済み予定かチェック
checkSchedulesIsNotExistsOnkintone: function(scheduleID) {
  var dataRequestkintoneApp = {
    app: kintone.app.getId(),
    fields: ['$id'],
    query: UPDATE_KEY_FIELD_CODE + ' like "' + scheduleID + '"',
    totalCount: true
  };

  return kintoneSDKRecord.getRecords(dataRequestkintoneApp).then(function(response) {
    if (!response.records || response.records.length === 0) {
      return true;
    }
    return false;
  });
},

// 登録処理呼び出し
addScheduleIntoKintone: function(data) {
  var kintoneRecord = outlookAPI.setParam(data);
  return outlookAPI.addKintone(kintoneRecord);
},

// 更新処理呼び出し
updateScheduleIntoKintone: function(data) {
  var kintoneRecord = outlookAPI.setParam(data);
  return outlookAPI.updateKintone(kintoneRecord, data.id);
},
~
};
```

#### 取得したデータを設定する

```kintone-connect-outlook-schedule.js
var outlookAPI = {
~
setParam: function(data) {
  var kintoneRecord = {};
  // Subject
  kintoneRecord[SUBJECT_FIELD_CODE] = {
    value: data.subject
  };
  // From
  kintoneRecord[START_DATE_FIELD_CODE] = {
    value: data.start.dateTime.substr(0, 19)
  };
  // To
  kintoneRecord[END_DATE_FIELD_CODE] = {
    value: data.end.dateTime.substr(0, 19)
  };
  // Details
  kintoneRecord[BODY_FIELD_CODE] = {
    value: data.body.content
  };
  // WorkTime （工数を算出する）
  var sDate = new Date(data.start.dateTime.substr(0, 19));
  var eDate = new Date(data.end.dateTime.substr(0, 19));
  var sTime = sDate.getHours() * 60 + sDate.getMinutes();
  var eTime = eDate.getHours() * 60 + eDate.getMinutes();
  var workTime = (eTime - sTime) / 60;
  kintoneRecord[WORKTIME_FIELD_CODE] = {
    value: workTime
  };
  // Assignees（ログインユーザを固定で設定）
  kintoneRecord[ASSIGNEES_FIELD_CODE] = {
    value: [{code: storage.getItem('SIGN_USER_DISPINFO')}]
  };
  // eventId（OutlookのイベントID）
  kintoneRecord[EVENT_ID_FIELD_CODE] = {
    value: data.id
  };
  // updateKey（OutlookのイベントIDの末尾64桁）
  kintoneRecord[UPDATE_KEY_FIELD_CODE] = {
    value: data.id.substr(data.id.length-64)
  };
  // isAllDay
  var allday = [];
  if (data.isAllDay) {
    allday.push("終日");
  }
  kintoneRecord[ALLDAY_FIELD_CODE] = {
    value: allday
  };
  return kintoneRecord;
},
~
};
```

#### 登録処理

```kintone-connect-outlook-schedule.js
var outlookAPI = {
~
// kintoneへ登録
addKintone: function(postParam) {
  var param = {
    app: kintone.app.getId(),
    record: postParam
  };
  
  return kintoneSDKRecord.addRecord(param);
},
~
};
```

#### 更新処理

```kintone-connect-outlook-schedule.js
var outlookAPI = {
~
// kintoneへ更新
updateKintone: function(putParam, eventId) {
  // 更新キーはレコード情報から削除する
  delete putParam[UPDATE_KEY_FIELD_CODE];

  // 更新キーを設定
  var updateKey = {
    field: UPDATE_KEY_FIELD_CODE,
    value: eventId.substr(eventId.length-64)
  }

  var param = {
    app: kintone.app.getId(),
    updateKey: updateKey,
    record: putParam
  };
  
  return kintoneSDKRecord.updateRecordByUpdateKey(param);
},
~
};
```

## サンプルの機能ではまだ微妙なのでさらに改修します
▼改修内容はこちら
- [KintoneとOutlookカレンダーを連携して工数を集計する #2](https://qiita.com/iNaoki04/items/87993dcdde34ca20e5b8)

## トラブルシューティング

エラー
アクセストークンが期限切れと怒られる
→ログアウトしてもっかいログインする

```json
{
  "error": {
    "code": "InvalidAuthenticationToken",
    "message": "Access token has expired.",
    "innerError": {
      "request-id": "********-****-****-****-************",
      "date": "2020-06-04T06:52:27"
    }
  }
}
```