フリーランスエンジニア向けの稼働管理ツールを作ってみた【GAS】

はじめまして、ますみです！

[![自己紹介.png](https://www.umi-mori.jp/wp-content/uploads/2023/02/introduction_banner_2-2048x533.png)](https://bit.ly/banner_intro_masumi_creator_qiita)


本記事は、「身の回りの困りごとを楽しく解決！【PR】Works Human Intelligence」のアドベントカレンダー2日目の記事です🎄

今回は、フリーランスエンジニア向けのツールとして**「自分の稼働時間を管理するツール」**の作り方と使い方を紹介します👨‍💻

私自身は4年ほどフリーランスとして、受託開発やコーチングなどの業務などをしてきましたが「実際にどれくらい各案件ごとに稼働しているのか？」を把握することは非常に重要だと3年目あたりから気付き始めました。

理由としては、以下の3つです。
1. 自分の時給単価（市場価値）を正確に把握するため。
2. 自分のタイムリソースのマネジメントのため。
3. 請求業務を効率化するため。

これに関しては、また別の機会に詳しく書きたいと思います！

いずれにせよ、稼働時間の管理をしっかりとしていない（もしくは全く管理していない）フリーランスエンジニアは意外と周りにいます。そういった方々の働く上での課題を解決するために10分くらいで運用を始められる管理ツールについてまとめてみました！

# 最終成果物
前置きが長くなりましたが、最終成果物は以下のようなものです。
少し早めのクリスマスプレゼントとして、ソースコードも全て公開していきます🎁
![成果物.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240280/0b3c45cd-d4b2-defe-b6c9-6b8e02bfa7f7.jpeg)

https://github.com/Masumi-M/attendance-record-gas-jp

# 使用技術
今回使用する技術（ツール）は以下の3つです。
1. Google Spreadsheet（スプレッドシート）
2. Google Calendar（カレンダー）
3. Google Apps Script（GAS、ガス）

カレンダーの勤怠情報を、GASを使って抽出して、スプレッドシートへ書き込むという流れになります。

GASが初めての方はこちらをご参照ください👍

https://workspace.google.co.jp/intl/ja/products/apps-script/

https://www.astina.co/media/system/post-1137

# 稼働管理ツールの作り方と手順
それでは、具体的な手順を説明していきます✨

## 1. 新しいGoogleスプレッドシートの作成
まずGoogleへログインして、新しいスプレッドシートを作ります。
すごく雑学ですが、実は`sheets.new`というURLから新規シートを作成することもできます:sunglasses:

![ss-editor-gas-work-calendar.001.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240280/abac4528-49db-f8a0-8589-177d4040f279.jpeg)

## 2. 大枠を作る
次に、取得してきた情報を見やすくするための大枠を作ります。
以下のように各行のタイトルと取得したい年月のシート名を入力します。

![ss-editor-gas-work-calendar.002.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240280/e2df18aa-465e-c3d6-612a-d0d210e8ba2a.jpeg)

テンプレートのスプレッドシートを以下のURLに置いておきました👍

https://docs.google.com/spreadsheets/d/1K72PnPBoq6HKObDIB4rwPHo8-mu6JYOLnWMClk_DMrk/edit

## 3. GASでコーディング
「拡張機能 > Apps Script」からGASの編集画面を開きます。

![ss-editor-gas-work-calendar.003.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240280/b6ad513a-c63f-1d0b-bfab-e02a8e241900.jpeg)

編集画面が開いたら以下の画像のように下記のコードを入力して、保存します。

![ss-editor-gas-work-calendar.004.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240280/5387a489-c08b-67c5-28ed-3554ef59ddc6.jpeg)

```js:コード.gs
const ORIGINAL_MENU_NAME                = '勤怠管理メニュー';
const GET_CALENDAR_INFO_BUTTON_NAME     = '勤怠情報の取得';
const TITLE_NAME                        = '勤怠管理';
const COMPLETE_PROCESS_TITLE            = '勤務表作成完了';
const COMPLETE_PROCESS_MESSAGE          = 'お仕事お疲れ様でした。';
const ERROR_TITLE                       = 'エラー発生';
const ERROR_MESSAGE_INVALID_SHEET_NAME  = '無効なシート名です。';
const ERROR_MESSAGE_NO_DATA             = '指定月の稼働は、現在ありません。';
const CALENDAR_ID                       = 'ENTER_HERE@group.calendar.google.com';

/*
【コーディング・ルール】
- 関数：キャメルケース（例：updateCalendarInfo()）
- グローバル定数：コンスタントケース（例：ORIGINAL_MENU_NAME）
- その他の定数および変数：スネークケース（例：sheet_name）
*/

function onOpen() {
  // オリジナルメニューの追加
  // TIPS: スプレッドシートが開いたタイミングで実行されます。
  let ui = SpreadsheetApp.getUi();
  ui.createMenu(ORIGINAL_MENU_NAME)
    .addItem(GET_CALENDAR_INFO_BUTTON_NAME, 'updateCalendarInfo')
    .addToUi();
}

function updateCalendarInfo() {
  try{
    // タイトルの記入（B2セルへ記入）
    const sheet_name = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getName();  // 選択中（アクティブ）のシート名を取得
    const sheet_title = TITLE_NAME + "（" + sheet_name + "）";  // 月の情報をタイトルに付加
    SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(2,2).setValue(sheet_title); // B2セルへタイトルを書き込み

    // カレンダーの取得
    const my_calendar = CalendarApp.getCalendarById(CALENDAR_ID);

    // 選択月の初日と末日を取得
    const calc_month_string = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getName() + "/01";
    const start_day = new Date(calc_month_string + " 00:00:00");
    const end_day = new Date(start_day.getFullYear(), start_day.getMonth()+1, 1);
    if (start_day == "Invalid Date"){ // エラー処理：シートの名前が正しくない時
      throw(ERROR_MESSAGE_INVALID_SHEET_NAME);
    }

    // 選択月のカレンダー内の情報（イベント）を取得
    const events = my_calendar.getEvents(start_day, end_day);
    if (events.length == 0){  // エラー処理：イベントが何もない時
      throw(ERROR_MESSAGE_NO_DATA);
    }

    // カレンダー情報の変数初期化
    let record_list = [];       // 予定情報の配列（List）
    let start_time;             // 開始時間（Date）
    let end_time;               // 終了時間（Date）
    let schedule_name = "";     // 予定の名前（String）
    let work_time = 0.0;        // 勤務時間（Float）
    let mtg_time = 0.0;         // MTG時間（Float）
    let work_time_total = 0.0;  // 勤務合計時間（Float）
    let mtg_time_total = 0.0;   // MTG合計時間（Float）

    for(const event of events){
      start_time = event.getStartTime();
      end_time = event.getEndTime();
      schedule_name = event.getTitle();
      work_time = (end_time - start_time)/(60*60*1000);
      if(schedule_name.match(/MTG/)){
        mtg_time = work_time;
      }else{
        mtg_time = 0;
      }

      const record = [
        start_time,
        end_time,
        schedule_name,
        work_time,
        mtg_time,
      ];
      record_list.push(record);

      work_time_total = work_time_total + work_time;
      mtg_time_total = mtg_time_total + mtg_time;
    }

    // レコードを書き込み
    // TIPS: 一行ずつ書き込むと時間がかかるため、このように配列に格納して、最後にまとめて記入します。
    SpreadsheetApp.getActiveSheet().getRange(4,2,record_list.length, record_list[0].length).setValues(record_list);

    // 合計時間（全勤務およびMTG）の書き込み
    SpreadsheetApp.getActiveSheet().getRange(2,5).setValue(work_time_total);  // B5セルへ勤務合計時間を書き込み
    SpreadsheetApp.getActiveSheet().getRange(2,6).setValue(mtg_time_total);   // B6セルへMTG合計時間を書き込み

    Browser.msgBox(COMPLETE_PROCESS_TITLE, COMPLETE_PROCESS_MESSAGE, Browser.Buttons.OK);
    return;
  }catch(e){
    Browser.msgBox(ERROR_TITLE, e, Browser.Buttons.OK);
    return;
  }
}
```

## 4. Google Calendarの作成とIDの埋め込み
以下の手順に沿って、勤怠管理用のGoogle Calendarを作成します。

![ss-editor-gas-work-calendar.005.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240280/f552876a-d13d-858e-2b7f-e924cc01678a.jpeg)
![ss-editor-gas-work-calendar.006.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240280/7c708504-c1e9-7112-4305-2e0cf67844f3.jpeg)
![ss-editor-gas-work-calendar.007.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240280/a9737a5d-213b-6ebe-90d9-cf65abb91a72.jpeg)

カレンダーが作成できたら、設定画面から、以下の手順でカレンダーIDをコピーします。
![ss-editor-gas-work-calendar.008.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240280/dc3b14e1-e569-7ef1-3c86-27a7022428f3.jpeg)
![ss-editor-gas-work-calendar.009.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240280/0bb473b8-3127-caf3-6012-322f6679fca4.jpeg)

コピーしたカレンダーIDをGASの`CALENDAR_ID`の箇所にペーストします。
![ss-editor-gas-work-calendar.010.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240280/72e2fc69-e995-dddd-23c1-26ddb8b5dca0.jpeg)

## 5. テスト
テストは開発において非常に重要です。
一つずつしっかりと動作しているか確認していきましょう🚀

### 5-1. 成功パターン
以下のようにサンプルの予定を追加してみましょう。
![ss-editor-gas-work-calendar.011.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240280/b79328ae-8e20-d256-04bb-acaf3dceaa94.jpeg)

「勤怠管理メニュー > 勤怠情報の取得」を選択して、処理を実行しましょう。
![ss-editor-gas-work-calendar.012.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240280/e203084b-d6e6-caee-47a8-b6008906d889.jpeg)

以下のように完了画面が表示されれば、成功です！
![ss-editor-gas-work-calendar.013.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240280/b3b2c2c5-cb8f-e50d-5833-62bd331cd157.jpeg)

追加した予定がしっかりと反映されているかも確認しましょう。
今回のソースコードでは、`MTG`という文字列が入っている場合にMTG時間としても出力されるようにプログラミングされています。
![ss-editor-gas-work-calendar.014.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240280/4a5ae007-df62-bf3c-43c6-15dca7ea7b40.jpeg)

### 5-2. 例外処理パターン①：シート名が無効な場合
続いて、シート名が`2021/12`のように年月のフォーマットではない場合のエラー処理を確認します。
以下のように無効な名前のシート名に変更して、失敗するか確認してみましょう。
![ss-editor-gas-work-calendar.015.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240280/477f317b-e517-99f3-9565-7c7dc1e6c2b8.jpeg)

以下のようにエラー発生の文章を表示されれば、OKです！
![ss-editor-gas-work-calendar.016.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240280/370f508a-950a-e451-e8d6-6f8311939af8.jpeg)

### 5-3. 例外処理パターン②：予定が何も入っていない場合
続いて、カレンダーに何も予定が入っていない場合のエラー処理を確認します。
以下のように、カレンダーに何も予定が入っていない年月のシート名に変更して、失敗するか確認してみましょう。
![ss-editor-gas-work-calendar.017.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240280/b8733b6c-c14c-01be-6288-5915cf7694b1.jpeg)

以下のようにエラー発生の文章を表示されれば、OKです！
![ss-editor-gas-work-calendar.018.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240280/286ee69d-cfd0-bf57-8f9d-90840373b41c.jpeg)

# おすすめの管理方法
最後に、この記事で紹介したスプレッドシートのおすすめの管理方法を紹介します。
私は、`2021/temp`というテンプレートのシートを作成して、それをコピーして、各月の勤務管理シート（月次勤怠表のようなもの）を生成するようにしています。
![ss-editor-gas-work-calendar.019.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240280/3491c017-f455-3426-10f1-c21d52f7a92c.jpeg)

# 最後に
最後まで読んでくださり、ありがとうございました！
いかがだったでしょうか？

この記事を通して、少しでもあなたの学びに役立てば幸いです！

:::note info
【仕事の相談はこちら】
お仕事の相談のある方は、下記のフォームよりお気軽にご相談ください。

[問い合わせフォームはこちら](https://forms.gle/G5g1SJ7BBZw7oXYA7)

もしもメールでの問い合わせの方がよろしければ、下記のメールアドレスへご連絡ください。

info*galirage.com（*を@に変えてご送付ください）

🎁 「生成AI活用の無料相談券」もしくは「生成AIの社内ガイドライン（無料PDF）」を『公式LINE』で配布中 🎁
『生成AIを業務に活用したいけど、どうしたらいいかわからない』といった声を多くいただきます。

Galirageでは公式LINEにて、チャットやオンライン会議で「完全個別の生成AI活用無料相談会」を実施しております！
（期間限定で実施しているため、ご興味ある方はお早めに以下のLINE公式アカウントをご登録ください^^）
https://lin.ee/rvz6lMN

※ 予告なく、キャンペーンを終了する可能性がございますが、ご了承ください。

【業務内容】
具体的には、以下のお仕事を中心に受け付けております！（[詳しくはこちら](https://galirage.notion.site/Galirage-AI-c2f3945fafb0433d8d2fd03e56927fd8)）
1. 受託開発（例：生成AIを使った社内システムの開発）
2. コンサルティング（例：技術戦略のアドバイス）
3. 講演（例：社内研修、イベント登壇）

※ 特に「生成AIを使ったシステム開発のご依頼」が多く、ご好評いただいております。

【これまでの相談事例】
以下のようなご相談が多くあります。

🔑 機密情報を漏洩させないための、生成AIのシステム構築をお願いしたい。
🤖 自社データを用いたFAQチャットボットの作り方を知りたい。
💡 ChatGPTを、自分たちの事業にどのように活かせるか、アドバイスやアイデアが欲しい。
:::
# おまけ
エンジニアの仲間（データサイエンティストも含む）を増やしたいため、公式LINEを始めました🎉

「**一緒に仕事をしてくれる方**」「**友だちとして仲良くしてくれる方**」は、友だち追加をしていただけますと嬉しいです！（仲良くなった人たちを集めて、「ボードゲーム会」や「ハッカソン」や「もくもく会」もやりたいなと考えています😆）

とはいえ、みなさんにもメリットがないと申し訳ないので、特典を用意しました！

友だち追加後に、アンケートに回答してくれた方へ「**エンジニア図鑑（職種20選）**」のPDFをお送りします◎

https://lin.ee/hFUN4TL

[![](https://www.umi-mori.jp/wp-content/uploads/2023/10/line_banner_v2.png)](https://lin.ee/hFUN4TL)