定型業務を完全制覇！GASで作る究極のタスク自動生成システム

## タスク管理どうされていますか

日々のタスク管理どのようにしていますか。
私はGoogleのToDoリストを使いやすくするChrome拡張を作り、それで管理しています。

個人的には、かなり使い勝手は良く、ストア公開したいのですがGoogle Tasks APIは機密性が高いAPIに分類されるため、ストア公開するにはかなりのお金をかけて審査機関へ審査依頼する必要があるので公開できてません・・・。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3741957/46afe84a-0e8e-034d-a33c-e4e088256d38.png)


## 会社の定型業務のタスク作成を自動化したい
会社で毎月勤怠の申請や、経費精算があります。
毎月の最終営業日にその作業を行うことになるのですが、Googleカレンダーでは毎月の繰り返し条件に最終営業日のような指定はできません。
Googleカレンダーでできるのはせいぜい「毎月30日」「毎月最終x曜日」のようなタスクで、祝日の考慮もされません。

なので手動でタスクを追加することになるのですが、これはかなり手間です。

## GASで定型業務のタスクを自動作成

毎月手動登録も芸がないので、Google Tasks APIを手軽に使えるGASからタスクを登録するようにしています
以下のページで、**新しいプロジェクト** をクリックします

https://script.google.com/home

プロジェクト名は何でも良いです。私は**定型業務タスク**としました
`コード.gs` の中身を以下に置き換えます

```typescript
function main() {
  const holidayCalendarId = 'ja.japanese#holiday@group.v.calendar.google.com';
  const processedDateKey = 'processedDate';
  const timeZone = 'Asia/Tokyo';

  const today = new Date();
  const { targetMonth, lastDate } = getNextMonthDates(today);
  const processedDate = new Date(PropertiesService.getScriptProperties().getProperty(processedDateKey));

  if (processedDate >= lastDate) {
    return;
  }

  const holidays = getHolidays(holidayCalendarId, targetMonth, lastDate, timeZone);

  const notifyDate = getNextBusinessDay(new Date(targetMonth.getFullYear(), targetMonth.getMonth(), 15), holidays);
  if (!isTaskAlreadyExists('勤怠・経費精算', notifyDate)) {
    addTask('勤怠・経費精算', notifyDate);
  }

  if (today.getMonth() === 5 || lastDate.getMonth() === 5) {
    const juneLastDate = getLastBusinessDay(new Date(today.getFullYear(), 5, 30), holidays);
    if (!isTaskAlreadyExists('年度末経費精算', juneLastDate)) {
      addTask('年度末経費精算', juneLastDate);
    }
  }

  PropertiesService.getScriptProperties().setProperty(processedDateKey, lastDate.toISOString());
}

function getNextMonthDates(today) {
  const nextMonth = today.getMonth() === 11 ? 0 : today.getMonth() + 1;
  const nextYear = today.getMonth() === 11 ? today.getFullYear() + 1 : today.getFullYear();
  
  const targetMonth = new Date(nextYear, nextMonth, 1);
  const lastDate = new Date(nextYear, nextMonth + 1, 0);

  return { targetMonth, lastDate };
}

function getHolidays(holidayCalendarId, start, end, timeZone) {
  const holidayEvents = CalendarApp.getCalendarById(holidayCalendarId).getEvents(start, end, { timeZone: timeZone });
  return holidayEvents.map(event => event.getStartTime());
}

function isBusinessDay(date, holidays) {
  const day = date.getDay();
  return day !== 6 && day !== 0 && !holidays.some(h => h.getTime() === date.getTime());
}

function getNextBusinessDay(date, holidays) {
  while (!isBusinessDay(date, holidays)) {
    date.setDate(date.getDate() + 1);
  }
  return date;
}

function getLastBusinessDay(date, holidays) {
  while (!isBusinessDay(date, holidays)) {
    date.setDate(date.getDate() - 1);
  }
  return date;
}

function addTask(title, dueDate) {
  Tasks.Tasks.insert({
    title: title,
    due: Utilities.formatDate(dueDate, "JST", "yyyy-MM-dd'T'HH:mm:ss") + ".000Z"
  }, Tasks.Tasklists.list().items[0].id);
}

function isTaskAlreadyExists(title, dueDate) {
  const taskList = Tasks.Tasklists.list().items[0];
  const tasks = Tasks.Tasks.list(taskList.id).items;

  const dueDateString = Utilities.formatDate(dueDate, "JST", "yyyy-MM-dd'T'HH:mm:ss") + ".000Z";

  return tasks.some(task => task.title === title && task.due === dueDateString);
}
```

### 工夫ポイント

1. getNextBusinessDay関数
この関数は、与えられた日付から次の営業日を見つけます。休日や週末を考慮して、次の営業日を返します。

```javascript
function getNextBusinessDay(date, holidays) {
  while (!isBusinessDay(date, holidays)) {
    date.setDate(date.getDate() + 1);
  }
  return date;
}
```

2. getLastBusinessDay関数
これは逆に、与えられた日付から前の営業日を見つけます。月末から遡って最後の営業日を探すのに使います。

```javascript
function getLastBusinessDay(date, holidays) {
  while (!isBusinessDay(date, holidays)) {
    date.setDate(date.getDate() - 1);
  }
  return date;
}
```

3. isTaskAlreadyExists関数
この関数は、同じタイトルと期日のタスクが既に存在するかをチェックします。これで重複登録を防いでいます。

```javascript
function isTaskAlreadyExists(title, dueDate) {
  const taskList = Tasks.Tasklists.list().items[0];
  const tasks = Tasks.Tasks.list(taskList.id).items;

  const dueDateString = Utilities.formatDate(dueDate, "JST", "yyyy-MM-dd'T'HH:mm:ss") + ".000Z";

  return tasks.some(task => task.title === title && task.due === dueDateString);
}
```

4. スクリプトプロパティ
スクリプトが最後に処理した日付を記録するために使います。これにより、一度削除したタスクが再度登録されることを防ぎます。

```javascript
const processedDateKey = 'processedDate';
const processedDate = new Date(PropertiesService.getScriptProperties().getProperty(processedDateKey));

if (processedDate >= lastDate) {
  return;
}

// ... (タスク作成処理)

PropertiesService.getScriptProperties().setProperty(processedDateKey, lastDate.toISOString());
```

これで、毎日実行しても不要なタスクの作成を避けられます。

### Tasks APIを使えるようにする

`コード.gs` を書き換えたら、Tasks APIを使えるようにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3741957/aa60640d-5fb0-7a4d-5ec6-0e8812afad88.png)

サービスの **+** をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3741957/dc1dd5c4-e610-a115-d86a-6fecc384297d.png)

**Google Tasks API ドキュメント** を選択し、**追加**をクリックします。

### アクセス権限を承認する
Googleカレンダーによる祝日判定とTasks APIを利用しているためアクセス権限を承認する必要があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3741957/b2c1f099-6170-d190-4d67-91e77e18b05a.png)


**実行**をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3741957/7f43eb7f-b6f0-113b-3682-5ffa57080010.png)


**権限を確認**をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3741957/524de198-2835-1c8d-f417-ddff32debe41.png)


**詳細**をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3741957/efb11942-837e-907c-3e47-ba8b7b886b92.png)

**<プロジェクト名> (安全ではないページ) に移動**をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3741957/3a240f6b-810b-db6c-9a4a-c4bac5e73052.png)


**許可**をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3741957/b2c1f099-6170-d190-4d67-91e77e18b05a.png)

再度**実行**をクリックします。

これで以下のような実行ログが出れば完了です。

![スクリーンショット 2024-06-26 10.55.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3741957/8286c1e8-e04b-81db-fdec-f981c25a2cd7.png)


### 作成されたタスクを確認する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3741957/873acd8f-710a-d3ec-6465-466ffeb222be.png)


6月は年度末の精算があるので、最終営業日にタスクが追加されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3741957/f628c8ad-3dfa-5636-1915-942872fe2ae8.png)

7月分のタスクも追加されています。ちゃんと祝日判定されていますね。

### 定期実行されるようにする

この状態では毎月**実行**をクリックする必要があるので、実行を自動化します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3741957/37122533-1783-f22c-7c39-2c0d42daefdb.png)


**トリガー**をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3741957/ca61e235-b1ab-9715-3773-a309fcf5c88b.png)


**時間ベースのトリガーのタイプを選択**で、**日付ベースのタイマー**を選択。
**時刻を選択**は何でも良いです。
**保存＊＊をクリックします。

これで毎日実行されるようになりました。

## まとめ

これで毎月の定型業務のタスクを自動で登録することができるようになりました。
GASはサーバレスで手軽にGoogleのAPIが使えるので良いですよね。
毎月x日以降の直近の営業日、毎月x日以前の直近の営業日を取得するメソッドを作成しましたが、ChatGPTに投げれば他の日付計算も簡単にできると思います。
タスク管理を効率的に行って日々の作業忘れを防ぎましょう。