9
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-05-19

Qiita_MV.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


事前準備

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

  1. スプレッドシートを開き、「挿入」メニューから「図形描画」を選択、好きな形でボタンを描画します。
    Qiita_事前準備01.png
      

  2. 作成した図形をクリックし、右上の「︙」メニューから
    「スクリプトを割り当て」 を選択します。
    Qiita_事前準備02.png
      

  3. 実行したい関数名(今回の記事だと 例:outputTogglTasks)を入力して保存します。
    Qiita_事前準備03.png

TogglのAPIトークンを取得

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

  1. Toggl Trackのプロフィールページ(https://track.toggl.com/profile)にアクセスします。
  2. 「API token」をコピー(Click to reveal部分)します。
    Qiita_事前準備04.png

GASにAPIキーを設定

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

  1. Google Apps Script の左にあるメニューから
    「歯車マーク > プロジェクトの設定」 を開き、ページ内のスクリプトプロパティに移動します。
    Qiita_事前準備05.png
      
  2. togglApiKey というキー名で、先ほど取得したトークンを登録します。

Qiita_事前準備06.png

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

完成コード全文

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で取得した作業記録を一時的に格納する配列や、スプレッドシートの出力開始行、作業時間の合計をカウントするための変数など、後続の処理で必要になる情報を初期化しています。

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

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から作業データを取得し、エラーチェックを行った後、複数の関数(データの整形・工数の集計・スプレッドシートへの出力)を呼び出しながら、一括して処理します。

// 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形式でレスポンスを受け取ります。

// 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レスポンスが返されます。実際に取得されるデータの例は次の通りです。

実際に取得できるデータ
{
  "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 }
  ]
}

Toggl APIから取得したデータで今回必要な項目

  • description: タスクの詳細名(例:「会議」「コーディング」など)
  • start: タスクの開始時刻
  • stop: タスクの終了時刻

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


4. 工数集計処理:calculateTotalHours()

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

// 合計工数とタスクごとの工数を計算
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以外は合計時間を赤字にします。

// 工数に応じた警告判定
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()

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

// 本文の出力
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()

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

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

8. 午前・終日日報の分岐:isNowNoonTime()

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

// 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の便利さも改めて実感できて、
個人的にもいい勉強になりました。

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


9
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?