金曜の夜、夜勤明けのくたくたな頭で、カレンダーを睨みながら足し算をしていました。「この打ち合わせは1時間半だったか、2時間だったか……」。クライアントに送る「今週の稼働時間」を出すためです。
凛です。本業の看護師と並行して、時給制の副業をいくつか抱えています。週末になると決まって「今週は何時間稼働しましたか?」と聞かれるのですが、これがなかなかの曲者でした。カレンダーの予定を一件ずつ足していくのに毎回10〜15分。しかも記憶が曖昧で、出した数字に自信が持てない。請求の根拠になる数字を、疲れた頭の記憶頼りで出す——本来いちばん避けたい状態です。この工数集計をGASに丸投げできないか、というのが今回の話です。
つらかったのは「時間」より「数字への不安」
最初は「集計に10分かかるのが面倒」だと思っていました。でも掘り下げてみると、本当につらかったのは所要時間そのものではなく、出した数字を信じきれないことでした。
金曜の疲れた頭で「これ1時間半だっけ、2時間だっけ」と思い出しながら足すと、どこかで1件くらい取り違えている気がして、送信ボタンを押すのがいつも少しためらわれる。請求につながる数字なのに、根拠が「たぶんこれくらい」では落ち着きません。
原因ははっきりしていました。人間の記憶に頼っているからです。カレンダーには正確な開始・終了時刻がちゃんと残っているのに、それを目で追って手で足すから、そこで誤差と不安が生まれる。だったら、予定の長さを機械的に積み上げてもらえばいい。そう考えると、やることは一気にシンプルになりました。
集計を成立させる下ごしらえ:タイトルの命名ルール
コードの前に、ひとつだけ準備があります。このGASを活かすには、カレンダーのイベントタイトルにクライアント名を含める命名ルールを決めておくことです。
私が使っているのは 【クライアント名】作業内容 の形。
【A社】WordPressカスタマイズ【B社】打ち合わせ【A社】コードレビュー【個人】ブログ執筆
こうしておくと、GASが 【】 の中身をクライアント名として自動で抜き出し、クライアント別に集計してくれます。この命名を普段の予定登録で徹底しておくのが、実は一番のコツかもしれません。
集計を任せたコード
ここが解決策の本体です。先週分をクライアント別に足し上げて、メールで送るところまでを1つの関数にしています。
/**
* 先週の稼働時間をクライアント別に集計してメール送信
* ※静的検証済み:GAS環境(V8ランタイム)で動作確認
*/
function weeklyReport() {
// 集計期間:先週(7日前〜今日)
const end = new Date();
const start = new Date(end.getTime() - 7 * 86400000); // 86400000 = 1日のミリ秒
// デフォルトカレンダーから予定を取得
const events = CalendarApp.getDefaultCalendar().getEvents(start, end);
// クライアント別の稼働時間を格納するオブジェクト
const summary = {};
events.forEach(e => {
// 終日イベントはスキップ(稼働時間の集計対象外)
if (e.isAllDayEvent()) return;
const title = e.getTitle();
// 時間を時間単位で計算(ミリ秒 → 時間)
const hours = (e.getEndTime() - e.getStartTime()) / 3600000;
// タイトルの【】からクライアント名を抽出
// 例:「【A社】打ち合わせ」→ 「A社」を取得
const match = title.match(/【(.+?)】/);
const client = match ? match[1] : 'その他';
// 同じクライアントの時間を合計
summary[client] = (summary[client] || 0) + hours;
});
// レポートのテキストを組み立て
const startStr = start.toLocaleDateString('ja-JP', { month: 'numeric', day: 'numeric' });
const endStr = end.toLocaleDateString('ja-JP', { month: 'numeric', day: 'numeric' });
let report = `【先週の稼働サマリ】\n${startStr} 〜 ${endStr}\n\n`;
// 稼働時間の多い順に並べて出力
Object.entries(summary)
.sort(([, a], [, b]) => b - a)
.forEach(([client, hours]) => {
report += `${client}: ${hours.toFixed(1)}h\n`;
});
// 合計も追加
const total = Object.values(summary).reduce((sum, h) => sum + h, 0);
report += `\n合計: ${total.toFixed(1)}h`;
// メールで送信
GmailApp.sendEmail(
'your@email.com', // ← 自分のメールアドレスに変更
'[週次] 工数レポート',
report
);
Logger.log('週次工数レポートを送信しました:\n' + report);
}
タイトルの 【】 を正規表現で拾って、クライアントごとに時間を足し込み、多い順に並べてメールする。処理としてはこれだけです。あの金曜夜の足し算が、まるごとこの数十行に置き換わりました。
スプシに履歴を残したいなら
メールは「今週いくら」を知るには十分ですが、月末に振り返りたくなると履歴が欲しくなります。その場合はスプシに書き足していく版が便利です。
/**
* 週次稼働レポートをスプシに記録する(累積管理版)
* ※静的検証済み:GAS環境(V8ランタイム)で動作確認
*/
function weeklyReportToSheet() {
const SPREADSHEET_ID = 'あなたのスプシのID'; // ← 変更してください
const SHEET_NAME = '工数記録';
const end = new Date();
const start = new Date(end.getTime() - 7 * 86400000);
const events = CalendarApp.getDefaultCalendar().getEvents(start, end);
const summary = {};
events.forEach(e => {
if (e.isAllDayEvent()) return;
const hours = (e.getEndTime() - e.getStartTime()) / 3600000;
const match = e.getTitle().match(/【(.+?)】/);
const client = match ? match[1] : 'その他';
summary[client] = (summary[client] || 0) + hours;
});
// スプシに書き込み
const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
const sheet = ss.getSheetByName(SHEET_NAME) || ss.insertSheet(SHEET_NAME);
// ヘッダーがなければ追加(初回のみ)
if (sheet.getLastRow() === 0) {
sheet.appendRow(['週', 'クライアント', '稼働時間(h)', '記録日時']);
}
// 週の表示用文字列(例:2026/05/11〜05/17)
const weekLabel = `${start.toLocaleDateString('ja-JP')}-${end.toLocaleDateString('ja-JP')}`;
// 各クライアントの行を追加
Object.entries(summary).forEach(([client, hours]) => {
sheet.appendRow([
weekLabel,
client,
hours.toFixed(1),
new Date().toLocaleString('ja-JP')
]);
});
Logger.log(`${Object.keys(summary).length} 件のクライアントデータをスプシに記録しました`);
}
毎週勝手に届くようにする
手で実行していては結局「金曜に忘れる」問題が残ります。トリガーで自動化して、レポートが向こうから届く状態にしてしまいましょう。
- GASエディタを開く(スプシ上部メニュー「拡張機能」→「Apps Script」)
- 左メニューの時計アイコン「トリガー」をクリック
- 右下の「+ トリガーを追加」ボタンをクリック
- 「実行する関数を選択」で
weeklyReportを選ぶ - 「イベントのソースを選択」で「時間主導型」を選ぶ
- 「時間ベースのトリガーのタイプを選択」で「週タイマー」を選ぶ
- 実行する曜日を「月曜日」に設定(週明けに先週の集計が届く)
- 実行時刻を「午前7時〜8時」に設定
- 「保存」ボタンをクリック
- Googleアカウントの認証画面が出たら「許可」をクリック
金曜の夜に受け取りたいなら、曜日を「金曜日」・時刻を「午後6時〜7時」に。私はこちらにしていて、退勤後にスマホを見るとレポートが届いている状態にしています。
運用してわかった細かいコツ
しばらく使ってみて、最初はわからなかったポイントが3つありました。
タイトルの【】方式に落ち着いた理由
最初はタイトルに番号をつける方式(「1:A社」「2:B社」)を試したのですが、パッと見でどこの案件かわからず挫折しました。【】 でくくる方式に変えたら、カレンダー上でも見やすいし、GAS側も match(/【(.+?)】/) の1行で抽出できて一石二鳥。命名ルールは「人間が見て自然」であることが続けるコツだと思います。
ミリ秒の割り算に慣れる
(getEndTime() - getStartTime()) / 3600000 で時間単位が取れます。3600000 は1時間のミリ秒(3600秒×1000)。日単位なら 86400000 で割ります。最初はこの数字に面食らいますが、「/ 3600000 で時間になる」とだけ覚えておけば大丈夫でした。
終日イベントを外し忘れない
isAllDayEvent() で終日イベントを除くと、純粋な稼働時間だけが残ります。私は「有給」「休日」「子どもの行事」を終日イベントで入れているので、これをスキップしないと数字がふくれ上がります。実際、最初のバージョンでこれを外し忘れて「今週80時間稼働!?」と目を疑ったことがありました。
つまずきやすいポイント
休憩時間まで稼働に入ってしまう
「9-18時、休憩1時間」の運用だと、そのまま足すと休憩込みになります。対策は2つ。
- カレンダーに「休憩」イベントを別途登録して、集計時に
if (title.includes('休憩')) return;でスキップする - タイトルに
[休憩除く8h]のようにメモを入れて、後から手で調整する
私は2番のメモ方式です。正確さより手間の少なさを取っています。細かく作り込んでも、続かなければ意味がないので。
個人予定まで稼働扱いになる
仕事用と個人用のカレンダーが混ざると、「子どもの習い事 2時間」まで稼働に入ってしまいます。対策は、仕事用カレンダーだけを対象にすること。
// 特定カレンダーだけを対象にする場合
const WORK_CALENDAR_ID = 'カレンダーID'; // カレンダー設定から確認できます
const cal = CalendarApp.getCalendarById(WORK_CALENDAR_ID);
const events = cal.getEvents(start, end);
カレンダーIDはGoogleカレンダーの「カレンダーの設定」→「カレンダーID」から確認できます。
週の区切りが9時間ずれる
タイムゾーン設定がUTCのままだと、週の区切りが9時間ずれて「月曜0時〜日曜9時」のような妙な範囲になります。GASプロジェクトの設定(⚙️歯車アイコン)でタイムゾーンを「(GMT+09:00) Asia/Tokyo」にすれば直ります。
導入して変わったこと
いま私の工数管理はこの流れで回っています。
| ステップ | 内容 |
|---|---|
| 日常 | カレンダーに 【クライアント名】作業内容 形式で予定を登録 |
| 毎週月曜7時(自動) | GASが先週分を自動集計 |
| 月曜朝(自動) | メールで工数レポートが届く |
| 月曜朝(任意) | クライアントにレポートを転送または報告 |
| 月末(任意) | スプシ版を使っていれば累積データを振り返る |
一番変わったのは、金曜夜の足し算がまるごと消えたことです。そして地味に大きいのが、数字への不安がなくなったこと。カレンダーの正確な時刻から機械的に積み上げた数字なので、送信ボタンを迷わず押せます。
「客先報告の準備が金曜の30分を奪う」——看護師をしながら副業もしている身にとって、この週30分の解放はかなり効きました。
関連記事(あわせて読みたい)
カレンダー自動化をもっと深めたい方は、以下の記事もおすすめです。
- GASでGoogleカレンダーに予定登録する最短10行コード — カレンダー登録の基本構文
- GASでGoogleカレンダーの今日の予定を毎朝メール通知する — 朝の通知自動化
- カレンダー×スプシ自動同期の入門 — 双方向同期テクニック
これらと組み合わせると、カレンダー運用が一気にラクになります。
この記事を書いた人:凛
2児のママで現役ナース。夜勤明けの細切れ時間を副業GASに投じ、月5〜8万円の副収入を継続中。「看護師でもコードは書ける」を合言葉に、家事育児とプログラミングを両立する等身大の情報を発信しています。
掲載しているコードは構文チェック済みですが、実際の運用ではお使いのカレンダー構成に合わせて調整してください。