MENU

【GAS】Toggl→Slack&スプレッドシートに転送する・プロジェクト指定可能

GASでTogglからSlackとスプレッドシートに転記する記事のアイキャッチ画像
悩む男性

Togglでの記録を、Slackに自動で飛ばせたら便利そうだな〜

上司に稼働報告をするとき、Togglの記録をSlackに自動で転送できたら便利そうですよね。

また、スプレッドシートにも転記されたら、時間の集計もできて、うれしいと思います。

このページでは、TogglからSlackとスプレッドシートに転送するGASを紹介します。

コピペして使ってみてくださいね。

目次

仕様

仕様
  • 前日のTogglの記録をSlackとスプレッドシートに転送する
  • 転送するのは、指定したプロジェクトのみ
  • Slackの転送先のスレッドを指定できる
  • 転送先スプレッドシートのテンプレあり

コード

スクリプトファイルごとに、書いています。

GASのスクリプトエディター上で、スクリプトファイルを6つ作って、それぞれにコピペして使ってください。

global

/**
 * スプレッドシートの設定
 */
const SPRREADSHEET_URL = '***スプレッドシートのURL***';
const SHEET_NAME = '***シートの名前***';


/**
 * Toggl APIの設定
 */
//  TogglのAPIキーをスクリプトプロパティから取得する
const TOGGL_API_KEY = PropertiesService.getScriptProperties().getProperty('toggl_api_key');
// 抽出するプロジェクト名を指定する
const PROJECT_NAME = '***抽出するプロジェクト名***';
// Toggl APIのURL
const TOGGL_API_URL = 'https://api.track.toggl.com/api/v8/time_entries';


/**
 * Slackの設定
 */
// 稼働時間を報告するチャンネルのチャンネルID
const CHANNEL_ID_FOR_REPORT = '***稼働時間を報告するチャンネルのチャンネルID***';
// 稼働時間を報告するチャンネルのWebhook URL
const REPORT_CHANNEL_WEBHOOK_URL = '***Webhook URL***';
// 稼働時間を報告するスレッドの親投稿のURL
const THREAD_URL = '***スレッドのURL***';
// 稼働時間を報告するスレッドのタイムスタンプ
const THREAD_TS = getThreadTimestampFromSlackURL(THREAD_URL);


/**
 * 前日の日付を取得する
 */
const YESTERDAY_DATE = getYesterdayDate();
function getYesterdayDate() {
  const today = new Date();
  const yesterday = new Date(today);
  yesterday.setDate(today.getDate() - 1);
  return yesterday.toISOString().split('T')[0];
}


/**
 * スレッドのタイムスタンプを取得する
 */
function getThreadTimestampFromSlackURL(url) {
  const match = url.match(/p(\d{10})(\d{6})$/);

  if (!match) {
    throw new Error('Invalid Slack URL format.');
  }

  console.log(`${match[1]}.${match[2]}`);
  return `${match[1]}.${match[2]}`;
}

01_main

/**
 * メイン
 */
function main() {

  // 指定したプロジェクトの記録をTogglから取得する
  const entries = getTogglEntriesForProject();
  // スプレッドシートに記録を入力する
  inputEntriesOnSpreadSheet(entries);
  // Slackに転送するメッセージを作る
  const formattedMessage = formatEntriesForSlack(entries);
  // Slackに投稿する
  postToSlack(formattedMessage);

}

02_getTogglEntriesForProject

/**
 * タイムエントリーの中から、指定してプロジェクトのみを抽出する
 */
// 指定したプロジェクト名 かつ 前日のタイムエントリーを抽出する
function getTogglEntriesForProject() {

  // プロジェクトIDを取得する
  const projectId = getProjectIdForName();

  // 抽出条件文をつくる
  const startDate = `${YESTERDAY_DATE}T00:00:00Z`;
  const endDate = `${YESTERDAY_DATE}T23:59:59Z`;
  const filteredURL = `${TOGGL_API_URL}?start_date=${startDate}&end_date=${endDate}`;

  // 前日のタイムエントリーを取得する
  const entries = fetchFromTogglAPI(filteredURL);
  console.log(entries);
  Logger.log(entries);

  // 指定したプロジェクトIDのエントリーのみを抽出する
  return entries.filter(entry => entry.pid === projectId);

}


// プロジェクト名からプロジェクトIDを取得する
function getProjectIdForName() {

  const workspaceId = getUserWorkspaceId();
  const PROJECTS_API_URL = `https://api.track.toggl.com/api/v8/workspaces/${workspaceId}/projects`;

  const projects = fetchFromTogglAPI(PROJECTS_API_URL);
  const project = projects.find(p => p.name === PROJECT_NAME);

  if (project) {
    return project.id;
  } else {
    throw new Error(`Project with name ${PROJECT_NAME} not found.`);
  }

}


// Toggl APIからユーザーのワークスペースを取得する
function getUserWorkspaceId() {

  const WORKSPACES_API_URL = 'https://api.track.toggl.com/api/v8/workspaces';

  const workspaces = fetchFromTogglAPI(WORKSPACES_API_URL);

  if (workspaces && workspaces.length > 0) {
    // 最初のワークスペースのIDを返す
    return workspaces[0].id;
  } else {
    throw new Error('No workspaces found for the user.');
  }

}


// Toggl APIへのリクエストを行う共通関数
function fetchFromTogglAPI(url) {

  const headers = {
    'Authorization': 'Basic ' + Utilities.base64Encode(TOGGL_API_KEY + ':api_token'),
    'Content-Type': 'application/json'
  };

  const options = {
    'method': 'GET',
    'headers': headers
  };

  const response = UrlFetchApp.fetch(url, options);
  return JSON.parse(response.getContentText());

}

03_inputEntriesOnSpreadSheet

/**
 * Togglのエントリーをスプレッドシートに出力する
 * 出力する項目
 * ・開始日:entry.start
 * ・開始時刻:entry.start
 * ・終了日:entry.stop
 * ・終了時刻:entry.stop
 * ・経過時間:entry.duration
 * ・内容:entry.description
 */
function inputEntriesOnSpreadSheet(entries) {

  // スプレッドシートを取得する
  let spreadSheet = SpreadsheetApp.openByUrl(SPRREADSHEET_URL);
  // シートを取得する
  let logSheet = spreadSheet.getSheetByName(SHEET_NAME);
  // データのある最終行の行番号を取得する
  let lastRow = getLastRow(logSheet);

  // Togglのエントリーズから、エントリーを取り出して、各項目に分けていく
  let data = entries.map(entry => {

    // UTC時間の開始日と開始時刻を日本時間になおす
    let [jpnStartDate, jpnStartTime] = formatDateTimeToJPN(entry.start);
    // UTC時間の終了日と終了時刻を日本時間になおす
    let [jpnStopDate, jpnStopTime] = formatDateTimeToJPN(entry.stop);
    // 経過時間(秒)を時分秒になおす
    const formattedDuration = formatDuration(entry.duration);

    // スプレッドシートの列の順番でdataに格納する
    // 開始日、開始時刻、終了日、終了時刻、内容、空欄(スプシ上の分類単語が入る)、経過時間
    return [jpnStartDate, jpnStartTime, jpnStopDate, jpnStopTime, entry.description, '', formattedDuration];
  });

  // シートにdataを出力する
  logSheet.getRange(lastRow + 1, 1, data.length, 7).setValues(data);

}

// UTC時間を日本時間になおす
function formatDateTimeToJPN(utcDateOfString) {

  // string形の日時を時間型になおす
  let utcDateOfTime = new Date(utcDateOfString);
  // UTC時間を日本時間になおす(9時間を足す)
  utcDateOfTime.setHours(utcDateOfTime.getHours() + 9);

  // 日付と時刻を配列で返す
  return [
    utcDateOfTime.toISOString().split('T')[0], utcDateOfTime.toISOString().slice(11, 19)
  ];

}


// データの入ってる最終行の行番号を取得する
function getLastRow(logSheet) {

  // シートの最終行を取得する
  let maxRow = logSheet.getMaxRows();
  // シートの最終行から、上向きにサーチして、データが入っている行を探す。
  let lastRow = logSheet.getRange(maxRow, 1).getNextDataCell(SpreadsheetApp.Direction.UP).getRowIndex();
  // その行番号を返す
  return lastRow;

}


// 秒数をhh:mm:ssに変換する
function formatDuration(durationInSeconds) {

  // 秒→時間
  const hours = Math.floor(durationInSeconds / 3600);
  // 秒→分
  const minutes = Math.floor((durationInSeconds % 3600) / 60);
  // 秒
  const seconds = durationInSeconds % 60;

  // hh:mm:ssの形式に変換する
  return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;

}

04_formatEntriesForSlack

function formatEntriesForSlack(entries) {

  return entries.map(entry => {
    return `・ ${entry.description}:${secondsToHMS(entry.duration)}`;
  }).join('\n');

}

// 秒数を hh時間mm分ss秒 の形式に変換する関数
function secondsToHMS(seconds) {

  const hours = Math.floor(seconds / 3600);
  seconds %= 3600;
  const minutes = Math.floor(seconds / 60);
  seconds %= 60;

  return `${hours}時間${minutes}分${seconds}秒`;

}

05_postToSlack

function postToSlack(message) {

  const payload = {
    'channel': CHANNEL_ID_FOR_REPORT,
    'username': 'Toggl_昨日の稼働報告',
    'icon_emoji' : ':alphabet-yellow-t:',
    'text': formatDateToMMDD(YESTERDAY_DATE) + '\n' + message
  };

  payload.thread_ts = THREAD_TS; // thread_tsをpayloadに追加

  const options = {
    'method': 'POST',
    'contentType': 'application/json',
    'payload': JSON.stringify(payload)
  };

  UrlFetchApp.fetch(REPORT_CHANNEL_WEBHOOK_URL, options);

}

// ISO形式の日付を MM/dd の形式に変換する関数
function formatDateToMMDD(isoDate) {

  const date = new Date(isoDate);
  const month = date.getMonth() + 1; // 月は0から始まるため、+1が必要
  const day = date.getDate();

  return `${month}/${day}`;

}

さいごに

私がテレワーク中に、稼働報告をするために作りました。

もし、自分じゃ実装できないという場合は、以下のページからお気軽にご連絡ください。

あなたの状況に合わせて、GASを実装します。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

コメントは日本語で入力してください。(スパム対策)

CAPTCHA

目次