名古屋のWeb活用アドバイザー

Googleカレンダーに登録されている作業内容を集計するには

GAS(Google Apps Script)ってご存じでしょうか?

Microsoft Officeで言うところの VBA(Visual Basic for Application)ですが、

Googleのサービス(Spread Sheet、GMail、カレンダーなど)においてプログラミングで制御できる仕組みです。

 

この前、知り合いの建築親方からAI活用の相談を受けたのですが、AIを使わなくても解決できる内容でした。

 



GASの内容は以下になります。

/************************************
 * 設定
 ************************************/

// 予定を取り出すカレンダーID
//  設定をしたいGoogleカレンダーを開いているGoogleアカウントと同じアカウントでSpreadSheatを開く必要があります。
var CAL_ID = Session.getActiveUser().getEmail();

// スプレッドシートID   Google Spread Sheat URLの /d/〜/ の部分
var SPREADSHEET_ID = '(編集するSpreadSheetのID)';

// シート名
var RAW_SHEET_NAME = 'events_raw';
var SUMMARY_SHEET_NAME = 'summary_by_client';
var INVALID_SHEET_NAME = '入力不備一覧';

// 集計メールの宛先
var TO_EMAIL = '(送信先Emailアドレス)';

// 会社別シート名の区切り文字(会社名_YYYY-MM)
var COMPANY_SHEET_SEP = '_';

/************************************
 * タイトル解析
 * 形式:[会社名][作業内容][金額]メモ
 ************************************/
function parseTitle_(title) {
  if (!title) return { client: '', work: '', amount: null, memo: '' };

  var normalized = title.replace(/[/g, '[').replace(/]/g, ']');

  var re = /^\s*\[([^\]]+)\]\s*\[([^\]]+)\]\s*\[([^\]]+)\]\s*(.*)$/;
  var m = normalized.match(re);
  if (m) {
    return {
      client: m[1].trim(),
      work: m[2].trim(),
      amount: parseAmount_(m[3]),
      memo: (m[4] || '').trim()
    };
  }

  // 旧形式救済
  var re2 = /^\s*\[([^\]]+)\]\s*\[([^\]]+)\]\s*(.*)$/;
  var m2 = normalized.match(re2);
  if (m2) {
    return { client: m2[1].trim(), work: m2[2].trim(), amount: null, memo: (m2[3] || '').trim() };
  }

  var re3 = /^\s*\[([^\]]+)\]\s*(.*)$/;
  var m3 = normalized.match(re3);
  if (m3) {
    return { client: m3[1].trim(), work: '', amount: null, memo: (m3[2] || '').trim() };
  }

  return { client: '', work: '', amount: null, memo: normalized.trim() };
}

function parseAmount_(s) {
  if (s === null || s === undefined) return null;
  var str = String(s).trim();
  if (!str) return null;

  str = str.replace(/[,\s]/g, '');
  str = str.replace(/[¥¥円]/g, '');
  str = str.replace(/[^0-9\-]/g, '');
  if (!str) return null;

  var n = Number(str);
  return isFinite(n) ? n : null;
}

/************************************
 * ヘッダー
 ************************************/
function setupRawHeader_(sheet) {
  var headers = [
    'Event ID',
    'Calendar ID',
    '日付',
    '開始時刻',
    '終了時刻',
    'タイトル(元)',
    '会社名',
    '作業内容',
    '金額',
    'メモ(タイトル)',
    'カレンダーの説明'
  ];
  sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
}

function setupInvalidHeader_(sheet) {
  var headers = [
    'Event ID',
    'Calendar ID',
    '日付',
    '開始時刻',
    '終了時刻',
    'タイトル(元)',
    '会社名',
    '作業内容',
    '金額',
    'メモ(タイトル)',
    'カレンダーの説明',
    '不備内容'
  ];
  sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
}

/************************************
 * 会社別(金額)集計
 ************************************/
function buildSummaryByClient_() {
  var ss = SpreadsheetApp.openById(SPREADSHEET_ID);
  var rawSheet = ss.getSheetByName(RAW_SHEET_NAME);
  var summarySheet = ss.getSheetByName(SUMMARY_SHEET_NAME);

  if (!summarySheet) summarySheet = ss.insertSheet(SUMMARY_SHEET_NAME);
  summarySheet.clear();

  if (!rawSheet) {
    summarySheet.getRange(1, 1).setValue('RAWシートがありません');
    return;
  }

  var lastRow = rawSheet.getLastRow();
  if (lastRow < 2) {
    summarySheet.getRange(1, 1).setValue('データがありません');
    return;
  }

  var data = rawSheet.getRange(2, 1, lastRow - 1, 11).getValues();
  var map = {}; // {会社名: { amount, count }}

  for (var i = 0; i < data.length; i++) {
    var client = data[i][6] || '(会社名未設定)'; // G
    var amount = data[i][8]; // I
    if (amount === null || amount === '' || isNaN(Number(amount))) amount = 0;

    if (!map[client]) map[client] = { amount: 0, count: 0 };
    map[client].amount += Number(amount);
    map[client].count += 1;
  }

  var rows = [['会社名', '件数', '合計金額']];
  Object.keys(map).forEach(function (k) {
    rows.push([k, map[k].count, map[k].amount]);
  });

  summarySheet.getRange(1, 1, rows.length, rows[0].length).setValues(rows);
  if (rows.length > 1) summarySheet.getRange(2, 3, rows.length - 1, 1).setNumberFormat('#,##0');
}

/************************************
 * 期間指定のイベント取り込み
 ************************************/
function exportEvents_(startDate, endDate) {
  var cal = CalendarApp.getCalendarById(CAL_ID);
  if (!cal) throw new Error('カレンダーが見つかりません: ' + CAL_ID);

  var events = cal.getEvents(startDate, endDate);
  var ss = SpreadsheetApp.openById(SPREADSHEET_ID);

  // RAW
  var rawSheet = ss.getSheetByName(RAW_SHEET_NAME);
  if (!rawSheet) rawSheet = ss.insertSheet(RAW_SHEET_NAME);
  setupRawHeader_(rawSheet);
  if (rawSheet.getLastRow() > 1) rawSheet.deleteRows(2, rawSheet.getLastRow() - 1);

  // 不備
  var invalidSheet = ss.getSheetByName(INVALID_SHEET_NAME);
  if (!invalidSheet) invalidSheet = ss.insertSheet(INVALID_SHEET_NAME);
  setupInvalidHeader_(invalidSheet);
  if (invalidSheet.getLastRow() > 1) invalidSheet.deleteRows(2, invalidSheet.getLastRow() - 1);

  var tz = Session.getScriptTimeZone();
  var rawValues = [];
  var invalidValues = [];

  for (var i = 0; i < events.length; i++) {
    var e = events[i];
    var start = e.getStartTime();
    var end = e.getEndTime();

    var title = e.getTitle();
    var parsed = parseTitle_(title);

    var rawRow = [
      e.getId(),
      CAL_ID,
      Utilities.formatDate(start, tz, 'yyyy/MM/dd'),
      Utilities.formatDate(start, tz, 'HH:mm'),
      Utilities.formatDate(end, tz, 'HH:mm'),
      title,
      parsed.client,
      parsed.work,
      parsed.amount,
      parsed.memo,
      e.getDescription() || ''
    ];
    rawValues.push(rawRow);

    var reason = '';
    if (!parsed.client) reason += '[会社名]が未設定。';
    if (!parsed.work) reason += '[作業内容]が未設定。';
    if (parsed.amount === null) reason += '[金額]が未設定または数値化できません。';

    if (reason) {
      var invalidRow = rawRow.slice();
      invalidRow.push(reason);
      invalidValues.push(invalidRow);
    }
  }

  if (rawValues.length) rawSheet.getRange(2, 1, rawValues.length, rawValues[0].length).setValues(rawValues);
  if (invalidValues.length) invalidSheet.getRange(2, 1, invalidValues.length, invalidValues[0].length).setValues(invalidValues);

  buildSummaryByClient_();
}

/************************************
 * 当月/先月の取り込み
 ************************************/
function exportThisMonthEvents() {
  var today = new Date();
  var first = new Date(today.getFullYear(), today.getMonth(), 1);
  var nextFirst = new Date(today.getFullYear(), today.getMonth() + 1, 1);
  exportEvents_(first, nextFirst);
}

function exportLastMonthEvents() {
  var today = new Date();
  var firstThis = new Date(today.getFullYear(), today.getMonth(), 1);
  var firstLast = new Date(today.getFullYear(), today.getMonth() - 1, 1);
  exportEvents_(firstLast, firstThis);
}

/************************************
 * 会社別シート名:会社名_YYYY-MM
 ************************************/
function getCompanySheetName_(clientName, year, month) {
  var ym = year + '-' + ('0' + month).slice(-2);
  var safeClient = String(clientName).replace(/[\\\/\?\*\[\]]/g, '_').trim();
  return safeClient + COMPANY_SHEET_SEP + ym;
}

/************************************************
 * 会社名 + 年月 を指定して会社別シートを作成
 * 出力列: 会社名 / 作業内容 / 金額 / メモ
 * + 最下行に合計(金額)
 ************************************************/
function createCompanySheetForMonth_(clientName, year, month) {
  var ss = SpreadsheetApp.openById(SPREADSHEET_ID);
  var rawSheet = ss.getSheetByName(RAW_SHEET_NAME);
  if (!rawSheet) throw new Error('RAWシートがありません: ' + RAW_SHEET_NAME);

  var lastRow = rawSheet.getLastRow();
  if (lastRow < 2) return;

  var data = rawSheet.getRange(2, 1, lastRow - 1, 11).getValues();

  var start = new Date(year, month - 1, 1);
  var end = new Date(year, month, 1);

  var rows = [];
  for (var i = 0; i < data.length; i++) {
    var r = data[i];
    var client = r[6];
    if (client !== clientName) continue;

    var dateStr = r[2];
    if (!dateStr) continue;

    var d;
    if (dateStr instanceof Date) {
      d = new Date(dateStr.getFullYear(), dateStr.getMonth(), dateStr.getDate());
    } else {
      var parts = String(dateStr).split(/[\/\-\.]/);
      if (parts.length < 3) continue;
      d = new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
    }
    if (d < start || d >= end) continue;

    rows.push([clientName, r[7], r[8], r[9]]);
  }

  if (!rows.length) return;

  // ★シート名は「会社名_YYYY-MM」
  var sheetName = getCompanySheetName_(clientName, year, month);

  // 同名があれば「上書き」したいので削除→再作成(当月再集計に強い)
  var existing = ss.getSheetByName(sheetName);
  if (existing) ss.deleteSheet(existing);
  var sheet = ss.insertSheet(sheetName);

  var header = ['会社名', '作業内容', '金額', 'メモ'];
  sheet.getRange(1, 1, 1, 4).setValues([header]);
  sheet.getRange(2, 1, rows.length, 4).setValues(rows);

  sheet.getRange(2, 3, rows.length, 1).setNumberFormat('#,##0');

  var lastDataRow = rows.length + 1;
  var totalRow = lastDataRow + 1;

  sheet.getRange(totalRow, 2).setValue('合計');
  sheet.getRange(totalRow, 3).setFormula('=SUM(C2:C' + lastDataRow + ')').setNumberFormat('#,##0');

  sheet.setColumnWidth(1, 160);
  sheet.setColumnWidth(2, 220);
  sheet.setColumnWidth(3, 110);
  sheet.setColumnWidth(4, 360);

  sheet.getRange(2, 2, rows.length, 2).setWrap(true);
  sheet.getRange(1, 1, lastDataRow, 4).setBorder(true, true, true, true, true, true);
  sheet.getRange(1, 1, 1, 4).setFontWeight('bold').setHorizontalAlignment('center');

  sheet.getRange(totalRow, 2).setHorizontalAlignment('right');
  sheet.getRange(totalRow, 3).setHorizontalAlignment('right');
}

/************************************************
 * 指定年月の会社別シートを全社分まとめて作成
 ************************************************/
function createAllCompanySheetsForMonth_(year, month) {
  var ss = SpreadsheetApp.openById(SPREADSHEET_ID);
  var rawSheet = ss.getSheetByName(RAW_SHEET_NAME);
  if (!rawSheet) throw new Error('RAWシートがありません: ' + RAW_SHEET_NAME);

  var lastRow = rawSheet.getLastRow();
  if (lastRow < 2) return [];

  var data = rawSheet.getRange(2, 1, lastRow - 1, 11).getValues();

  var clientsSet = {};
  for (var i = 0; i < data.length; i++) {
    var client = data[i][6];
    if (client) clientsSet[client] = true;
  }

  var clients = Object.keys(clientsSet);
  var created = [];

  for (var j = 0; j < clients.length; j++) {
    createCompanySheetForMonth_(clients[j], year, month);
    created.push(getCompanySheetName_(clients[j], year, month));
  }

  return created; // 作成したシート名一覧
}

/************************************************
 * 月末に当月分を集計するバッチ(手動実行も可)
 * 1) 当月分をevents_rawへ取り込み
 * 2) 当月分の会社別シートを全社分生成(会社名_YYYY-MM)
 * 3) Excelをメール添付(任意)
 ************************************************/
function monthlyBatchForThisMonth() {
  var ss = SpreadsheetApp.openById(SPREADSHEET_ID);

  // 当月の年月
  var today = new Date();
  var year = today.getFullYear();
  var month = today.getMonth() + 1;

  // 1) 当月取り込み
  exportThisMonthEvents();

  // 2) 当月の会社別シート全社生成
  var createdSheets = createAllCompanySheetsForMonth_(year, month);

  SpreadsheetApp.flush();

  var ymLabel = year + '年' + ('0' + month).slice(-2) + '月';
  var url = ss.getUrl();

  var subject = '【当月分カレンダー作業ログ(金額)】' + ymLabel;
  var body = '';
  body += ymLabel + 'のカレンダー作業ログ(金額)を集計しました。\n\n';
  body += 'スプレッドシート:\n' + url + '\n\n';
  body += '▼会社別シート一覧(シート名=会社名_YYYY-MM)\n';
  if (!createdSheets.length) {
    body += ' 当月分のデータがありませんでした。\n';
  } else {
    createdSheets.forEach(function (name) { body += ' ・' + name + '\n'; });
  }
  body += '\n';
  body += '※「入力不備一覧」シートに、[会社名][作業内容][金額]の入力ルールに合っていない予定が出力されています。\n';

  // Excel添付(必要なければ、このブロックを丸ごと削除してもOK)
  var exportUrl = 'https://docs.google.com/spreadsheets/d/' + SPREADSHEET_ID + '/export?format=xlsx';
  var params = {
    method: 'get',
    headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() },
    muteHttpExceptions: true
  };
  var response = UrlFetchApp.fetch(exportUrl, params);
  var blob = response.getBlob().setName('当月作業ログ_' + ymLabel + '.xlsx');

  MailApp.sendEmail({
    to: TO_EMAIL,
    subject: subject,
    body: body,
    attachments: [blob]
  });
}

/************************************************
 * 月末判定:今日が月末なら当月バッチを実行
 * (日次トリガーで呼ぶ想定)
 ************************************************/
function runIfMonthEnd() {
  var today = new Date();
  var tomorrow = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);

  // 明日が1日=今日が月末
  if (tomorrow.getDate() === 1) {
    monthlyBatchForThisMonth();
  }
}

/************************************************
 * トリガー自動設定(1回だけ手動実行)
 * 毎日21:00に runIfMonthEnd を実行
 ************************************************/
function installMonthEndTrigger() {
  // 既存トリガー整理(重複防止)
  var triggers = ScriptApp.getProjectTriggers();
  triggers.forEach(function (t) {
    if (t.getHandlerFunction() === 'runIfMonthEnd') {
      ScriptApp.deleteTrigger(t);
    }
  });

  ScriptApp.newTrigger('runIfMonthEnd')
    .timeBased()
    .everyDays(1)
    .atHour(21) // 21時(お好みで変更)
    .create();
}

/************************************************
 * メニュー(手動実行用)
 ************************************************/
function menuCreateCompanySheetsForYM() {
  var ui = SpreadsheetApp.getUi();

  var ymResp = ui.prompt('対象年月', '例:2025-12 のように入力してください', ui.ButtonSet.OK_CANCEL);
  if (ymResp.getSelectedButton() !== ui.Button.OK) return;

  var parts = ymResp.getResponseText().trim().split(/[-\/]/);
  var year = parseInt(parts[0], 10);
  var month = parseInt(parts[1], 10);

  // 指定月を取り込み→会社別全社生成
  var first = new Date(year, month - 1, 1);
  var nextFirst = new Date(year, month, 1);
  exportEvents_(first, nextFirst);

  createAllCompanySheetsForMonth_(year, month);
  SpreadsheetApp.flush();

  ui.alert('完了', '会社別シート(会社名_YYYY-MM)を作成しました。', ui.ButtonSet.OK);
}

function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('カレンダー連携')
    .addItem('当月分を取り込み', 'exportThisMonthEvents')
    .addItem('先月分を取り込み', 'exportLastMonthEvents')
    .addSeparator()
    .addItem('当月バッチ実行(当月取り込み→会社別全社→メール)', 'monthlyBatchForThisMonth')
    .addItem('指定年月で会社別全社シート作成(YYYY-MM)', 'menuCreateCompanySheetsForYM')
    .addSeparator()
    .addItem('月末自動集計トリガーを設定(1回だけ)', 'installMonthEndTrigger')
    .addToUi();
}

関連記事

この記事のハッシュタグに関連する記事が見つかりませんでした。

最新記事

カテゴリー

アーカイブ

ハッシュタグ