【Toggl Track × GAS】スプレッドシートに日報を自動書き出し！

![Qiita_MV.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4034734/e52fb914-d6eb-428d-a9b9-c07137f84249.png)

# はじめに

日々の業務をToggl Trackで記録しているけれど、
**日報を書くのが面倒・集計が手間** と感じていませんか？

本記事では、Google Apps Scriptを使って
**「自動でToggleからスプレッドシートに日報を出力する方法」** を紹介します。

業務時間のチェックや日報作成の時短に、ぜひ活用してください！


## 目次

1. [設計概要](#設計概要)
2. [事前準備](#事前準備)
   - スプレッドシートにボタンを作成してスクリプト割り当て
   - TogglのAPIトークンを取得
   - GASにAPIキーを設定
3. [完成コード全文](#完成コード全文)
4. [各処理の解説](#各処理の解説)
　- 1. 定数/変数の定義
　- 2. メイン処理：`outputTogglTasks()`
　- 3. API取得処理：`getTogglApiResponse()`
　- 4. 工数集計処理：`calculateTotalHours()`
　- 5. 警告表示：`judgmentWarning()`
　- 6. 出力：`outputDailyReport()`
　- 7. 出力補助：`setCellValue()`
　- 8. 午前・終日の分岐：`isNowNoonTime()`

5. [さいごに](#さいごに)

## 設計概要

- Toggl APIを使って当日の作業時間を取得
- 合計時間とタスク一覧を自動計算
- 午前・終日のフォーマットを自動切り替え
- 所定時間外なら合計時間を赤字に
- スプレッドシートに日報として出力

<完成図>

![Qiita_完成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4034734/0b473106-7066-4c61-924a-a4f43c5eb873.png)



---

## 事前準備

### スプレッドシートにボタンを作成してスクリプト割り当て

1. スプレッドシートを開き、「挿入」メニューから「図形描画」を選択、好きな形でボタンを描画します。
![Qiita_事前準備01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4034734/28ea60d2-5b28-45a3-9b27-752e7ff26840.png)
　　

2. 作成した図形をクリックし、右上の「︙」メニューから
   **「スクリプトを割り当て」** を選択します。
![Qiita_事前準備02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4034734/62460614-6555-4eb9-a22f-31253d873428.png)
　　
3. 実行したい関数名（今回の記事だと　例：`outputTogglTasks`）を入力して保存します。
![Qiita_事前準備03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4034734/930ea87c-4cf3-4d60-9fe2-161a42bcbc42.png)


### TogglのAPIトークンを取得

Toggl APIを使うために、TogglのAPIトークンを取得します。

1. Toggl Trackのプロフィールページ[(https://track.toggl.com/profile)](https://track.toggl.com/profile)にアクセスします。
2. 「API token」をコピー（Click to reveal部分）します。
![Qiita_事前準備04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4034734/caeee407-1e4a-4377-9325-95eefe542033.png)


### GASにAPIキーを設定

Google Apps Script (GAS) にAPIキーを設定します。

1. Google Apps Script の左にあるメニューから
   **「歯車マーク > プロジェクトの設定」** を開き、ページ内のスクリプトプロパティに移動します。
![Qiita_事前準備05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4034734/52862eda-2601-47d2-86a8-37c442ea6e86.png)
　　
2. `togglApiKey` というキー名で、先ほど取得したトークンを登録します。

![Qiita_事前準備06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4034734/5e72e315-5ad3-4944-941d-b87b44d08a9e.png)

:::note info
この設定を行うことで、スクリプトがToggl APIを利用できるようになります。
:::



## 完成コード全文

```javascript
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const togglApiKey = PropertiesService.getScriptProperties().getProperty('togglApiKey');
const today = new Date().toISOString().split('T')[0]; // 今日の日付を取得
const togglTodayData = `https://api.track.toggl.com/api/v9/me/time_entries?start_date=${today}T00:00:00%2B09:00&end_date=${today}T23:59:59%2B09:00`; // 今日の作業データを取得するためのAPIリクエスト
const taskMap = {}; // タスク名ごとの作業時間
let totalHours = 0; // 今日1日全体の作業時間の合計
let outputRow = 2; // 何行目から書くか
let togglTodayTasks = []; // Togglから取得した本日の作業データ

// Togglデータを日報形式で出力する
function outputTogglTasks() {

    togglTodayTasks = getTogglApiResponse(); // TogglのAPIレスポンスを取得
    sheet.clear(); // 既存の内容をクリア

    // 記録が空だったら注意文を表示
    if (!togglTodayTasks.length) {
        return setCellValue(sheet, 1, '⚠️ 本日のTogglデータはありません。');
    }

    // ストップウォッチが止まっていない場合処理を終了
    for (let i = 0; i < togglTodayTasks.length; i++) {
        if (!togglTodayTasks[i].stop) {
            return setCellValue(sheet, 1, `⚠️ 作業「${togglTodayTasks[i].description || '(no description)'}」が終了していません。Togglでストップウォッチを止めてください。`);
        }
    }
    // 工数計算
    calculateTotalHours();

    outputRow = setCellValue(sheet, outputRow, `合計${Math.round(totalHours * 100) / 100}時間です`);

    // 警告判定
    judgmentWarning();

    // 本文出力
    outputDailyReport();

    console.log(togglTodayTasks);

}

// TogglのAPIレスポンスを取得
function getTogglApiResponse() {
    // togglApiKeyが空だったらスプシの右下にポップアップ表示
    if (!togglApiKey) {
        SpreadsheetApp.getActiveSpreadsheet().toast('Toggl APIキーが未設定です。gasのスクリプトプロパティで設定してください。');
        return [];
    }

    // TogglのAPIにGETリクエストを送信
    const response = UrlFetchApp.fetch(togglTodayData, {
        method: 'get',
        headers: {
            'Content-Type': 'application/json', // JSON形式で
            'Authorization': 'Basic ' + Utilities.base64Encode(togglApiKey + ':api_token'), // Basic認証
        },
    });
    // 返ってきた作業記録をjsに変換
    return JSON.parse(response.getContentText());
}

// 合計工数とタスクごとの工数を計算
function calculateTotalHours() {
    togglTodayTasks.forEach(entry => {
        const name = entry.description || '(no description)'; // タスク名
        const durationHours = (new Date(entry.stop) - new Date(entry.start)) / (1000 * 60 * 60); // 工数（h）

        taskMap[name] = (taskMap[name] || 0) + durationHours; // 作業ごとの合計時間
        totalHours += durationHours; // 合計時間
    });
}

// 工数に応じた警告判定
function judgmentWarning() {
    // 合計時間が基準範囲に収まっているか判定し、条件に合わない場合は赤字にする
    if (isNowNoonTime()) {
        // 午前日報は1〜4時間以外は赤字
        if (totalHours < 1 || totalHours > 4) {
            sheet.getRange(outputRow - 1, 3).setFontColor('red');
        }
    } else {
        // 通常の日報は7〜12時間以外は赤字
        if (totalHours < 7 || totalHours > 12) {
            sheet.getRange(outputRow - 1, 3).setFontColor('red');
        }
    }
}

// 本文の出力
function outputDailyReport() {
    // 出力
    if (isNowNoonTime()) {
        [
            '',
            'お疲れ様です。',
            '本日在宅のため、午前中の日報をお送りします。 ',
            '',
            '■ 午前Todo',
            ...Object.keys(taskMap).map(task => `・${task}（${Math.round(taskMap[task] * 100) / 100}h）`),
            '',
            '■ 午後Todo',
            '・'
        ].forEach(line => {
            outputRow = setCellValue(sheet, outputRow, line);
        });
    } else {
        [
            '',
            'お疲れ様です。',
            '本日の日報お送りいたします。',
            '',
            '■業務',
            ...Object.keys(taskMap).map(task => `・${task}（${Math.round(taskMap[task] * 100) / 100}h）`),
            '',
            '■翌営業日やること',
            '・',
            '',
            '■所感'
        ].forEach(line => {
            outputRow = setCellValue(sheet, outputRow, line);
        });
    }
}

// 指定列に値を設定するための関数
function setCellValue(sheet, row, value) {
    sheet.getRange(row, 3).setValue(value);
    return row + 1; // 次の行に進む
}

// 12:00〜14:30の時間帯かどうかを判定（午前日報用）
function isNowNoonTime() {
    const now = new Date();//現在の時刻を取得
    const currentTime = now.getHours() + now.getMinutes() / 60;
    return currentTime >= 12 && currentTime < 14.5;
}


```



## 各処理の解説



### 1. 定数/変数の定義

まずはスクリプト全体で使用する定数や変数を定義しています。Toggl APIで取得した作業記録を一時的に格納する配列や、スプレッドシートの出力開始行、作業時間の合計をカウントするための変数など、後続の処理で必要になる情報を初期化しています。

:::note warn
※今回は私のタスクを管理する個人用のプロジェクトで
外部からの干渉がないのでグローバルで定義しています。
グループや他人と共有する際は、関数内スコープやモジュールを検討しましょう。
:::


```javascript
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); // スプレッドシートの指定
const togglApiKey = PropertiesService.getScriptProperties().getProperty('togglApiKey'); // APIキーの取得
const today = new Date().toISOString().split('T')[0]; // 今日の日付を取得
const togglTodayData = `https://api.track.toggl.com/api/v9/me/time_entries?start_date=${today}T00:00:00%2B09:00&end_date=${today}T23:59:59%2B09:00`; // 今日の作業データを取得するためのAPIリクエスト
const taskMap = {}; // タスク名ごとの作業時間
let totalHours = 0; // 今日1日全体の作業時間の合計
let outputRow = 2; // 何行目から書くか
let togglTodayTasks = []; // Togglから取得した本日の作業データ
```

---

### 2. メイン処理：`outputTogglTasks()`

この関数が日報出力のメイン処理を担っています。Togglから作業データを取得し、エラーチェックを行った後、複数の関数（データの整形・工数の集計・スプレッドシートへの出力）を呼び出しながら、一括して処理します。

```javascript
// Togglデータを日報形式で出力する
function outputTogglTasks() {

    togglTodayTasks = getTogglApiResponse(); // TogglのAPIレスポンスを取得
    sheet.clear(); // 既存の内容をクリア

    // 記録が空だったら注意文を表示
    if (!togglTodayTasks.length) {
        return setCellValue(sheet, 1, '⚠️ 本日のTogglデータはありません。');
    }

    // ストップウォッチが止まっていない場合処理を終了
    for (let i = 0; i < togglTodayTasks.length; i++) {
        if (!togglTodayTasks[i].stop) {
            return setCellValue(sheet, 1, `⚠️ 作業「${togglTodayTasks[i].description || '(no description)'}」が終了していません。Togglでストップウォッチを止めてください。`);
        }
    }
    // 工数計算
    calculateTotalHours();

    outputRow = setCellValue(sheet, outputRow, `合計${Math.round(totalHours * 100) / 100}時間です`);

    // 警告判定
    judgmentWarning();

    // 本文出力
    outputDailyReport();

}
```

---

### 3. API取得処理：`getTogglApiResponse()`

スクリプト内で`togglApiKey`が設定されているか確認します。設定されていない場合は、スプレッドシートにエラーメッセージを表示します。
次にToggl APIを利用して、当日の作業ログを取得します。ここでは、`UrlFetchApp.fetch()`を使ってAPIからデータを取得し、JSON形式でレスポンスを受け取ります。

```javascript
// TogglのAPIレスポンスを取得
function getTogglApiResponse() {
    // togglApiKeyが空だったらスプシの右下にポップアップ表示
    if (!togglApiKey) {
        SpreadsheetApp.getActiveSpreadsheet().toast('Toggl APIキーが未設定です。gasのスクリプトプロパティで設定してください。');
        return [];
    }

    // TogglのAPIにGETリクエストを送信
    const response = UrlFetchApp.fetch(togglTodayData, {
        method: 'get',
        headers: {
            'Content-Type': 'application/json', // JSON形式で
            'Authorization': 'Basic ' + Utilities.base64Encode(togglApiKey + ':api_token'), // Basic認証
        },
    });
    // 返ってきた作業記録をjsに変換
    return JSON.parse(response.getContentText());
}
```
Toggl APIを使って作業データを取得すると、以下のようなJSONレスポンスが返されます。実際に取得されるデータの例は次の通りです。

```json:実際に取得できるデータ
{
  "data": [
    { id: 3936030404,
        workspace_id: 9072306,
        project_id: null,
        task_id: null,
        billable: false,
        start: '2025-05-16T08:01:52+00:00',
        stop: '2025-05-16T08:56:55+00:00',
        duration: 3303,
        description: '勉強会',
        tags: [ '会議' ],
        tag_ids: [ 17382408 ],
        duronly: true,
        at: '2025-05-16T08:56:58.524968Z',
        server_deleted_at: null,
        user_id: 11640125,
        uid: 11640125,
        wid: 9072306 }
  ]
}
```
:::note info
Toggl APIから取得したデータで今回必要な項目
- `description`: タスクの詳細名（例:「会議」「コーディング」など）
- `start`: タスクの開始時刻
- `stop`: タスクの終了時刻
:::

これらの情報を元に、作業時間やタスク名を集計して日報を作成します。

---

### 4. 工数集計処理：`calculateTotalHours()`

各タスクの作業時間を集計し、合計時間を算出します。

```javascript
// 合計工数とタスクごとの工数を計算
function calculateTotalHours() {
    togglTodayTasks.forEach(entry => {
        const name = entry.description || '(no description)'; // タスク名
        const durationHours = (new Date(entry.stop) - new Date(entry.start)) / (1000 * 60 * 60); // 工数（h）

        taskMap[name] = (taskMap[name] || 0) + durationHours; // 作業ごとの合計時間
        totalHours += durationHours; // 合計時間
    });
}
```

---

### 5. 警告表示：`judgmentWarning()`

Toggleの手動修正した場合のミスをそのまま送るのを防ぐため、午前なら1〜4h以外、午後なら7〜12h以外は合計時間を赤字にします。

```javascript
// 工数に応じた警告判定
function judgmentWarning() {
    // 合計時間が基準範囲に収まっているか判定し、条件に合わない場合は赤字にする
    if (isNowNoonTime()) {
        // 午前日報は1〜4時間以外は赤字
        if (totalHours < 1 || totalHours > 4) {
            sheet.getRange(outputRow - 1, 3).setFontColor('red');
        }
    } else {
        // 終日の日報は7〜12時間以外は赤字
        if (totalHours < 7 || totalHours > 12) {
            sheet.getRange(outputRow - 1, 3).setFontColor('red');
        }
    }
}
```

---

### 6. 出力：`outputDailyReport()`

午前中または終日で日報形式を切り替え、スプレッドシートに出力します。

```javascript
// 本文の出力
function outputDailyReport() {
    // 出力
    if (isNowNoonTime()) {
        [
            '',
            'お疲れ様です。',
            '本日在宅のため、午前中の日報をお送りします。 ',
            '',
            '■ 午前Todo',
            ...Object.keys(taskMap).map(task => `・${task}（${Math.round(taskMap[task] * 100) / 100}h）`),
            '',
            '■ 午後Todo',
            '・'
        ].forEach(line => {
            outputRow = setCellValue(sheet, outputRow, line);
        });
    } else {
        [
            '',
            'お疲れ様です。',
            '本日の日報お送りいたします。',
            '',
            '■業務',
            ...Object.keys(taskMap).map(task => `・${task}（${Math.round(taskMap[task] * 100) / 100}h）`),
            '',
            '■翌営業日やること',
            '・',
            '',
            '■所感'
        ].forEach(line => {
            outputRow = setCellValue(sheet, outputRow, line);
        });
    }
}
```

---

### 7. 出力補助：`setCellValue()`

指定した行と列に値を設定し、次に進むための行番号を返します。

```javascript
// 指定列に値を設定するための関数
function setCellValue(sheet, row, value) {
    sheet.getRange(row, 3).setValue(value);
    return row + 1; // 次の行に進む
}
```

---

### 8. 午前・終日日報の分岐：`isNowNoonTime()`

時間帯が12:00〜14:30かで午前日報か・終日日報かを判定します。

```javascript
// 12:00〜14:30の時間帯かどうかを判定（午前日報用）
function isNowNoonTime() {
    const now = new Date();//現在の時刻を取得
    const currentTime = now.getHours() + now.getMinutes() / 60;
    return currentTime >= 12 && currentTime < 14.5;
}
```

---

## さいごに

毎日のルーティンである、ちょっと面倒くさい日報作成。
ボタンひとつでまとめてくれるのは、地味にありがたいです！

Toggl × GAS の連携を通じて、APIの扱いやGASの便利さも改めて実感できて、
個人的にもいい勉強になりました。

出力フォーマットやチェック条件は、業務スタイルに合わせて自由にカスタマイズできるので、
ぜひ使ってみてください！

---