Pleasanter 期限付きテーブルの情報をGoogleカレンダーに同期する

## はじめに
Pleasanterはカレンダー表示だけでなく、バーンダウンチャートやガントチャート、時系列チャートなどの機能も備えており、スケジュールやプロジェクトの管理、タスクの追跡などに幅広く活用できるツールです。

さらに、Googleカレンダーという世界的に普及しているスケジューリングツールとの連携は、情報の一元管理やスケジュールの効率化に役立つ方法です。この記事では、Pleasanterの期限付きレコードで登録した予定をGoogleカレンダーに同期させる方法について説明します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492700/64683231-721e-f3d2-a83c-14b3f6380545.png)

## Google側の設定
### Google Calendar API の設定
Googleカレンダーを操作するためには、Google Apps ScriptでCalendar APIを有効にする必要があります。Google Apps Scriptでは以下の手順でCalendarAPIを有効化します。

1. Apps Script プロジェクトの開始  
Google Driveから 新規 >  Google Apps Script を選択し、プロジェクトを新規作成します。

2. サービスの追加  
スクリプトエディタの左側にある「+」アイコン付きの「サービス」をクリックします。

3. カレンダーAPIの検索  
表示されたダイアログで「Google Calendar API」を選択し、「追加」 ボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492700/4f7f31f7-6b5a-c621-3b8d-13d795fa74d0.png)


### Googleカレンダー カレンダーIDの確認

続いて、Googleカレンダーを開き、連携対象とするカレンダーの「設定と共有」から「カレンダーの設定」にアクセスし、カレンダーの詳細セクションの下部にある`カレンダーID`をコピーします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492700/3ba8eb5e-1a50-0ac9-6193-3830d4d8d274.png)

### PropertiesServiceへカレンダーID格納
取得したカレンダーIDは、Google Apps ScriptのPropertiesServiceを使用して保存します。

1. スクリプトエディタを開く
Google Apps Scriptプロジェクトを開き、スクリプトエディタにアクセスします。  
 
2. ファイルメニューからプロジェクトのプロパティを選択
スクリプトエディタの上部にあるメニューバーから「ファイル」>「プロジェクトの設定」を選択します。

3. スクリプトプロパティの設定
開いたダイアログの中から「スクリプトプロパティを追加」ボタンクリックします。
表示された入力欄に、キーとなるプロパティと値を入力します。プロパティはここでは`calendarId`を、値には先ほど確認した`カレンダーID`を入力し、「スクリプトプロパティを保存」ボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492700/139b1f16-f489-dbb3-e522-99f302d98630.png)

### Google AppsScriptの記述

先の手順で作成したGoogle Apps Script のプロジェクトを開き、コード記述欄に以下のスクリプトを貼り付け、保存します。

```javascript  
// POSTリクエストを受け取って処理するdoPost関数
function doPost(e) {

  // PropertiesServiceからカレンダーIDを取得
  var calendarId = PropertiesService.getScriptProperties().getProperty('calendarId');

  // レスポンスのデフォルト値を定義
  var response = {
    status: "error",
    msg: "",
    id: ""
  };

  try {
    // リクエストからパラメータを取得
    var params = JSON.parse(e.postData.contents);

    // アクションとIDによって処理を分岐
    if (params.action === 'create' || !params.id) {

      // 新しいイベントを作成
      var newEvent = CalendarApp.getCalendarById(calendarId).createEvent(
        params.title,
        new Date(params.startDateTime),
        new Date(params.endDateTime),
        { description: params.description }
      );
      response.msg = "Event create";
      response.id = newEvent.getId();

    } else if (params.action === 'update' || params.action === 'updatebycalendar') {

      // イベントを取得して更新
      var eventToUpdate = CalendarApp.getCalendarById(calendarId).getEventById(params.id);
      if (eventToUpdate) {
        eventToUpdate.setTitle(params.title);
        eventToUpdate.setDescription(params.description);
        eventToUpdate.setTime(new Date(params.startDateTime), new Date(params.endDateTime));
        response.msg = "Event updated";
        response.id = eventToUpdate.getId();
      } else {
        response.msg = "Event not found";
      }

    } else if (params.action === 'delete') {

      // イベントIDを指定して削除
      var eventToDelete = CalendarApp.getCalendarById(calendarId).getEventById(params.id);
      if (eventToDelete) {
        eventToDelete.deleteEvent();
        response.msg = "Event deleted";
      } else {
        response.msg = "Event not found";
      }

    } else {
      // アクションの設定が不適切
      response.msg = "Invalid action";
    }

    // 処理が成功したかチェック
    if (response.msg.startsWith("Event")) {
      response.status = "success";
    }

  } catch (err) {
    // エラーが発生した場合、エラーメッセージをレスポンスにセット
    response.msg = "Error: " + err.toString();
  }

  // レスポンスをクライアントに返す
  return ContentService.createTextOutput(JSON.stringify(response))
    .setMimeType(ContentService.MimeType.JSON);
}
```
#### スクリプトの説明  
上記のコードは、PleasanterからのPOSTリクエストを処理し、リクエストで指定された条件に基づいてGoogleカレンダーの作成、更新、削除を行います。  

カレンダーを新規作成した際、更新と削除のキーとするため、作成した予定ごとに付与されるイベントIDをレスポンスとして返答します。  カレンダーを更新、削除する際は、リクエストに付与されたイベントIDをもとに対象のイベン特定したうえで処理をします。

### スクリプトのデプロイ
Google Apps Scriptで作成したdoPost関数を実際のWebアプリケーションとして機能させるためには、以下のようにデプロイ作業を行います。

1. スクリプトエディタの右上にある「デプロイ」ボタンをクリックし、「新しいデプロイ」を選択します。

2. デプロイタイプの選択
デプロイの設定画面で、「デプロイタイプ」のドロップダウンメニューから「Webアプリ」を選択します。

3. デプロイの詳細設定
   次のデプロイオプションを設定します。
   | オプション    | 説明                                          |
   |-----------|---------------------------------------------|
   | 説明         | デプロイのバージョンを識別する説明を入力（例：初回デプロイ） |
   | 実行ユーザー    | アプリを実行するユーザーを指定 |
   | アクセス       | アプリへのアクセスを誰に許可するかを選択  （例：全員） |

4. デプロイ
   設定が完了したら、「デプロイ」ボタンをクリックします。

5. WebアプリURLの取得
デプロイが成功すると、生成されたWebアプリケーションのURLが表示されるので、これをコピーします。このURLが外部からのPOSTリクエストを受け取るエンドポイントとなります。

## Pleasanterの設定

### 期限付きテーブルの作成

Pleasanterのサイトの新規作成で期限付きテーブル(Issues)を作成します。

|項目|名称|設定|
|--- |--- |--- |
| タイトル(Title)|タイトル ||
| 内容(Body)|説明文 ||
| 開始(StartTime)|開始日時 |エディタの書式: 日付と時刻(分)|
| 完了(CompletionTime)|終了日時 |エディタの書式: 日付と時刻(分)|
| 分類A(ClassA)|カレンダーID |読取専用|

### Pleasanterのサーバスクリプト設定

作成した期限付きテーブルのテーブルの管理、から「サーバスクリプト」を開き、新規のサーバスクリプトとして以下を記述します。 条件(実行の契機)は、`作成前`、`更新前`、`削除前` を選択します。
```javascript
// dataオブジェクトを初期化
let data = {};

// Pleasanterのコンテキスト、モデルからデータをセット
data.action = context.Action;           // アクション
data.title = model.Title;               // タイトル
data.description = model.Body;          // 説明文
data.startDateTime = model.StartTime;    // 開始時刻
data.endDateTime = model.CompletionTime; // 終了時刻
data.id = model.ClassA;               // カレンダーID

data.timeZone = 'Asia/Tokyo';         // タイムゾーン

// Google Apps ScriptのWebアプリケーションURLを設定
httpClient.RequestUri = '※前項で取得したWebアプリケーションのURLをここに記述';

// JSON形式の文字列に変換してHTTPリクエストの内容をセット
httpClient.Content = JSON.stringify(data);

// POSTリクエストを送信し、レスポンスを取得
let response = httpClient.Post();

// レスポンスをJSONオブジェクトにパース
let responseObject = JSON.parse(response);

// HTTPリクエストが成功したかどうかをチェック
if (httpClient.IsSuccess) {
    // ログに成功と応答の内容を記録
    context.Log('Success: ' + response);
    context.Log(responseObject.id);
    
    // ClassAに生成したイベントのカレンダーIDをセット
    model.ClassA = responseObject.id;
} else {
    // エラー時にステータスコードとレスポンス内容をログに記録
    context.Log('Error: (' + httpClient.StatusCode + ') ' + response);
}
```

また、同様に「サーバスクリプト」を開き、新規のサーバスクリプトとして以下を記述します。 条件(実行の契機)は、`画面表示の前` を選択します。

```javascript
elements.DisplayType('BulkDeleteCommand',3);
elements.DisplayType('EditImportSettings',3);
```

#### サーバスクリプトの説明

1つめのサーバスクリプトでは、期限付きレコードの作成、更新、削除処理に応じてレコードから取得したデータをGoogle Apps Scriptに送信します。  

Pleasanterサーバスクリプトのオブジェクト `context`、`model`で収集した各データを`httpClient.Post`で Google Apps ScriptにPOSTしています。

`context.Action` はそれぞれ処理に応じて作成( `create` )、更新( `update` )、カレンダー表示での予定の移動( `updatebycalendar` )、削除( `delete` ) の値が取得され、Google Apps Script側での処理の分岐に利用しています。
また、作成時に取得したGoogleカレンダーのイベントごとのIDは取得した分類A(ClassA)にカレンダーIDとして保存しています。


[開発者向け機能：サーバスクリプト：model | Pleasanter](https://pleasanter.org/manual/server-script-model)
[開発者向け機能：サーバスクリプト：context | Pleasanter](https://pleasanter.org/manual/server-script-context)
[開発者向け機能：サーバスクリプト：httpClient.Post | Pleasanter](https://pleasanter.org/manual/server-script-http-client-post)

:::note warn
タイムゾーンの指定については、記述を簡略化するため `Asia/Tokyo` と固定値で記述しています。日本以外のタイムゾーンを使う場合は、適宜Googleカレンダーに対応したタイムゾーンを指定する必要があります。

[Settings  |  Google Calendar  |  Google for Developers](https://developers.google.com/calendar/api/v3/reference/settings?hl=ja)

:::



2つめのサーバスクリプトは、上記処理が一括削除とインポートによる更新にに対応していないため、一覧画面上からボタンを無効化しています。

[開発者向け機能：サーバスクリプト：elements.DisplayType | Pleasanter](https://pleasanter.org/manual/server-script-elements-display-type)

## 動作イメージ
![Calendar_.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492700/ca941a05-0dac-58b3-9da6-f826b1643edb.gif)


ユーザーがPleasanterで新しい予定を作成すると、サーバスクリプトがこの情報をApps ScriptにPOSTリクエストとして送信し、「create」アクションが実行されて新しいカレンダーイベントが作成されます。更新があった場合は「update / updatebycalendar」アクションが、削除があった場合は「delete」アクションがそれぞれ実行されます。

## まとめ
本記事では、Pleasanterの期限付きデータをGoogle Apps Scriptを利用し、Googleカレンダーに同期させる方法について解説しました。