LoginSignup
0
2

More than 1 year has passed since last update.

スプレッドシートの内容をもとに議事録作成とカレンダー予定登録をGASで自動化する方法

Last updated at Posted at 2023-01-31

スプレッドシートから取得した内容に沿って、議事録テンプレート(Googleドキュメント)に日付を挿入して、その議事録リンクとGoogleMeet参加ボタンを載せたスケジュールをカレンダーに登録する…という一連の作業を毎月自動でやってくれるアプリを作ったので、共有します。

カレンダーを開いて繰り返し機能を設定して議事録コピペして日付を書いて…っていうのを、毎月手作業でガチャガチャやらなくてよくなるので、かなり便利だと思います。CUIに慣れるとGUIって煩わしくなってきますよね--;

必要な関数をコピペするなどして、お役立てください。

使ったスプレッドシート

登録内容の設定に使うスプレッドシートは、こんな感じです。
getSheetByNameで スプレッドシート内のシート を取得する必要があるため、シート名を分かりやすいものにしておいてください。

No 項目 内容
1 タイトル 定期ミーティング
2 開始時間 15:00
3 終了時間 16:00
4 曜日
5 リモート参加の可否(可/不可)
6 カレンダー種類 会議
7 議事録リンク(載せる/載せない) 載せる
8 備考 これは備考です。

タイトル・備考はご自由に設定していただいて構いません。
それ以外の項目については、以下のフォーマットの通り入力しなくてはならない仕様になっています。

【開始時間・終了時間】HH:mm
【曜日】月/火/水/木/金いずれか漢字一文字
【カレンダー種類】Googleカレンダーで「マイカレンダー」を設定し、その表示名を入力
()付きの項目は()内のいずれかの内容をご入力ください。

Googleドキュメント(議事録テンプレート)

原本はこんな感じです。
ドキュメントに対して行うことは以下の4つです。

➀タイトルの「議事録テンプレート」を「yyyy/M 定期ミーティング」に変更
➁議事録の最上部のテキスト「202X/00 定期ミーティング」を「yyyy/M 定期ミーティング」に変更
➂見出し2の「/」部分を月と曜日に合わせた日付に変更
➃Googleカレンダーの備考欄にリンクを貼り付ける(スプレッドシートの項目「議事録リンク(載せる/載せない)」が「載せる」となっている場合)

GASのソースコード

最後に、ソースコードです。
GASを開いた時に出てくるサイドメニューから、GoogleカレンダーAPIを入れることを忘れずに!
そうしないと、ミーティングが登録できません。

/** 関数化に使う年月を取得 */

// 今日の日付
const TODAY = new Date();
// 現在の西暦
const CURRENT_YEAR = TODAY.getFullYear();
// 来年の西暦
const NEXT_YEAR = CURRENT_YEAR + 1;
// 来月(登録する分の月, 0始まり)
const NEXT_MONTH = TODAY.getMonth() + 1;
// 再来月の初日(登録する分の月の翌月, 0始まり)
const MONTH_AFTER_NEXT = NEXT_MONTH + 1;
// テンプレートとするドキュメントIDを設定
let tempId = "任意のID";

function main() {
  let doc = CreateDocument();
  ReadSheet(doc);
  EditDocument(calendarDocDataObject);
  CreateSchedule(calendarDocDataObject);
}

/**
 * ドキュメントを作る関数
 * @return void
 */
function CreateDocument() {
  // コピーを作成し、そのidを取得
  let newDocId = DriveApp.getFileById(tempId).makeCopy().getId();
  let doc = { newDocId: newDocId }
  return doc;
}

/**
 * スプレッドシートを読み取ってドキュメント・スケジュール作成用のフォーマットに変更する関数
 * @param object doc : 作成したドキュメントのID
 * @return void
 */
function ReadSheet(doc) {

  // 読み取り範囲(表の始まり列と終わり行)
  const TOP_ROW = 1;
  const TOP_COL = 2;
  const NUM_OF_ROW = 9;
  // スプレッドシート内の行(0始まり)
  const TITLE_NUM = 1;
  const START_NUM = 2;
  const END_NUM = 3;
  const DAY_NUM = 4;
  const ATTEND_MEET_NUM = 5;
  const TYPE_OF_SCHEDULE_NUM = 6;
  const AGENDA_NUM = 7;
  const REMARKS_NUM = 8;

  // シートを取得
  const SHEET_ID = "任意のID";
  const SS = SpreadsheetApp.openById(SHEET_ID);
  const SHEET = SS.getSheetByName('任意のシート名');
  // 予定の一覧をバッファに取得
  const CONTENTS = SHEET.getRange(TOP_ROW, TOP_COL, TOP_ROW + NUM_OF_ROW - 1, TOP_COL).getValues();

  // ドキュメントやカレンダーの編集・作成用の値をセット
  const EVENT_TITLE = CONTENTS[TITLE_NUM][1];
  const DAY = CONTENTS[DAY_NUM][1];
  const START = CONTENTS[START_NUM][1];
  const END = CONTENTS[END_NUM][1];
  const ATTEND_MEET_OR_NOT = CONTENTS[ATTEND_MEET_NUM][1];
  const TYPE_OF_SCHEDULE = CONTENTS[TYPE_OF_SCHEDULE_NUM][1];
  const SHOW_AGENDA = CONTENTS[AGENDA_NUM][1];
  const REMARKS = CONTENTS[REMARKS_NUM][1];

  /**「カレンダー種類」からカレンダーを取得 */

  // スクリプト実行時と同じアカウント内のカレンダー種類を配列で取得
  // smileappアカウントで実行すると、smileappのカレンダーに予定が作成されるので注意
  let tentativeCalendar = CalendarApp.getCalendarsByName(TYPE_OF_SCHEDULE);
  // 要素からカレンダーIDを取得
  for (let i in tentativeCalendar) {
    var tentativeCalendarId = tentativeCalendar[i].getId();
  }
  // カレンダーIDからカレンダーを、Calendarオブジェクトで取得
  const CALENDAR_ID = tentativeCalendarId;
  const CALENDAR = CalendarApp.getCalendarById(CALENDAR_ID);

  /**繰り返しの最初の日付をセット*/

  // 翌月の初日
  const FIRST_DATE_OF_NEXT_MONTH = new Date(CURRENT_YEAR, NEXT_MONTH, 1);
  // 翌月初日の曜日
  const FIRST_DAY_OF_NEXT_MONTH = FIRST_DATE_OF_NEXT_MONTH.getDay();
  // 繰り返し終了日の翌日
  const FIRST_DATE_MONTH_AFTER_NEXT = new Date(CURRENT_YEAR, MONTH_AFTER_NEXT, 1);
  const FIRST_DATE_MONTH_AFTER_NEXT_FOR_MEET = FIRST_DATE_MONTH_AFTER_NEXT.toISOString().replace(/-/g, "").replace(/:/g, "").replace(".000", "");

  /** 曜日と繰り返しルールをセット */

  // 曜日番号を代入する用の変数
  let meetingDay;
  let recurrence;
  let Mo = "毎週月曜日";
  let Tu = "毎週火曜日";
  let We = "毎週水曜日";
  let Th = "毎週木曜日";
  let Fr = "毎週金曜日";

  // スプレッドシートから取得した文字列に応じて、曜日番号を取得
  // Meetがある場合とない場合の繰り返しルールを作成
  switch (DAY) {
    case Mo:
      meetingDay = 1;
      recurrenceForMeet = [`RRULE:FREQ=WEEKLY;BYDAY=Mo;UNTIL=${FIRST_DATE_MONTH_AFTER_NEXT_FOR_MEET}`];
      recurrence = CalendarApp.newRecurrence().addWeeklyRule().onlyOnWeekday(CalendarApp.Weekday.MONDAY).until(FIRST_DATE_MONTH_AFTER_NEXT);
      break;
    case Tu:
      meetingDay = 2;
      recurrenceForMeet = [`RRULE:FREQ=WEEKLY;BYDAY=Tu;UNTIL=${FIRST_DATE_MONTH_AFTER_NEXT_FOR_MEET}`];
      recurrence = CalendarApp.newRecurrence().addWeeklyRule().onlyOnWeekday(CalendarApp.Weekday.TUESDAY).until(FIRST_DATE_MONTH_AFTER_NEXT);
      break;
    case We:
      meetingDay = 3;
      recurrenceForMeet = [`RRULE:FREQ=WEEKLY;BYDAY=We;UNTIL=${FIRST_DATE_MONTH_AFTER_NEXT_FOR_MEET}`];
      recurrence = CalendarApp.newRecurrence().addWeeklyRule().onlyOnWeekday(CalendarApp.Weekday.WEDNESDAY).until(FIRST_DATE_MONTH_AFTER_NEXT);
      break;
    case Th:
      meetingDay = 4;
      recurrenceForMeet = [`RRULE:FREQ=WEEKLY;BYDAY=Th;UNTIL=${FIRST_DATE_MONTH_AFTER_NEXT_FOR_MEET}`];
      recurrence = CalendarApp.newRecurrence().addWeeklyRule().onlyOnWeekday(CalendarApp.Weekday.THURSDAY).until(FIRST_DATE_MONTH_AFTER_NEXT);
      break;
    case Fr:
      meetingDay = 5;
      recurrenceForMeet = [`RRULE:FREQ=WEEKLY;BYDAY=Fr;UNTIL=${FIRST_DATE_MONTH_AFTER_NEXT_FOR_MEET}`];
      recurrence = CalendarApp.newRecurrence().addWeeklyRule().onlyOnWeekday(CalendarApp.Weekday.FRIDAY).until(FIRST_DATE_MONTH_AFTER_NEXT);
      break;
  }

  /**日付・時刻・URLをセット */

  // 翌月初日の曜日を求める際に使う変数
  let addend = 0;

  // 翌月初日が日、月、火、水のいずれかの場合
  if (FIRST_DAY_OF_NEXT_MONTH <= meetingDay) {
    // 繰り返しの曜日を表す数 - 翌月初日の曜日
    addend = meetingDay - FIRST_DAY_OF_NEXT_MONTH;
  }
  // 翌月初日が木、金、土のいずれかの場合
  else {
    // 7-(翌月初日の曜日 - 繰り返しの曜日を表す数)
    addend = 7 - (FIRST_DAY_OF_NEXT_MONTH - meetingDay);
  }
  const FIRST_DATE_OF_MEETING = 1 + addend;
  // 月初のミーティングの日付を取得
  const START_TIME = new Date(CURRENT_YEAR, NEXT_MONTH, FIRST_DATE_OF_MEETING);
  const END_TIME = new Date(CURRENT_YEAR, NEXT_MONTH, FIRST_DATE_OF_MEETING);

  // 開始時刻と終了時刻
  START_TIME.setHours(START.getHours());
  START_TIME.setMinutes(START.getMinutes());
  END_TIME.setHours(END.getHours());
  END_TIME.setMinutes(END.getMinutes());
  // Meet用の日付フォーマット
  const START_TIME_FOR_MEET = Utilities.formatDate(START_TIME, "JST", "yyyy-MM-dd'T'HH:mm:ss");
  const END_TIME_FOR_MEET = Utilities.formatDate(END_TIME, "JST", "yyyy-MM-dd'T'HH:mm:ss");
  // MeetのURLを作成
  const REQUEST_ID = Math.random().toString(32).substring(2);

  /**備考欄を作成 */

  // カレンダーに表示する用のURLを取得
  newDoc = DriveApp.getFileById(doc.newDocId);
  let docLink = newDoc.getUrl();
  // "●備考"と備考欄の内容をまとめる(空欄の場合は"●備考"を表示しない)
  let remarks;
  if (REMARKS == "") {
    remarks = "";
  }
  else {
    remarks = "\n" + "●備考" + "\n" + REMARKS;
  }
  // URLと説明を組み合わせる
  let docAndRemark;
  if (SHOW_AGENDA == "ON") {
    docAndRemark = "●議題" + "\n" + docLink + remarks;
  }
  else if (SHOW_AGENDA == "OFF") {
    docAndRemark = "●備考" + "\n" + remarks;
  }

  /**カレンダー登録・ドキュメント編集用の引数 */

  calendarDocDataObject = {
    // カレンダー
    calendar: CALENDAR,
    // カレンダーID
    calendarId: CALENDAR_ID,
    // 予定のタイトル
    eventTitle: EVENT_TITLE,
    // ミーティング用の開始時刻フォーマット
    startTimeForMeet: START_TIME_FOR_MEET,
    // ミーティング用の終了時刻フォーマット
    endTimeForMeet: END_TIME_FOR_MEET,
    // ミーティング用の繰り返し設定
    reccurenceForMeet: recurrenceForMeet,
    // 開始時刻フォーマット
    startTime: START_TIME,
    // 終了時刻フォーマット
    endTime: END_TIME,
    // 繰り返し設定
    recurence: recurrence,
    // ミーティング登録する/しないの設定
    attendMeetOrNot: ATTEND_MEET_OR_NOT,
    // ミーティングのURL
    meetUrl: REQUEST_ID,
    // リンクと備考
    docAndRemark: docAndRemark,
    // ドキュメントID
    newDocId: doc.newDocId,
    // 処理が走る日から起算して再来月の月初
    firstDateMonthAfterNext: FIRST_DATE_MONTH_AFTER_NEXT,
  }
}

/**
 * ドキュメントを編集する関数
 * @param object calendarDocDataObject カレンダー登録とドキュメント編集に必要なデータ
 * @return void
 */
function EditDocument(calendarDocDataObject) {

  /**ドキュメントの移動 */

  // 原本のコピーを所定のフォルダに移動(二重以上の階層になっていても、一番下の階層のフォルダIDを指定すればよい)
  let newDoc = DriveApp.getFileById(calendarDocDataObject.newDocId);
  // 保守フォルダ(作成した議事録を入れるフォルダの一つ上の階層)のID
  const MAINTENANCE_FOLDER_ID = "";
  let maintenanceFolder = DriveApp.getFolderById(MAINTENANCE_FOLDER_ID);
  // 紛らわしいので移動
  newDoc.moveTo(maintenanceFolder);

  // 保守フォルダ内からその年のフォルダを探してファイルを入れる(なければ作る)
  let startYearFolderName = calendarDocDataObject.startTime.getFullYear() + "年";

  // ミーティング開始年を含むフォルダ群を取得
  let startYearFolders = DriveApp.getFolderById(MAINTENANCE_FOLDER_ID).getFoldersByName(startYearFolderName);
  // フォルダが見つかった場合
  if (startYearFolders.hasNext()) {
    // 開始年のフォルダを取得
    let startYearFolder = startYearFolders.next();
    newDoc.moveTo(startYearFolder);
  }
  // 見つからなかった場合(関数の作動月が12月の時)
  else {
    // 保守フォルダ直下に来年分のフォルダを作成して移動
    startYearFolder = maintenanceFolder.createFolder(startYearFolderName);
    newDoc.moveTo(startYearFolder);
  }

  /**ドキュメントファイルのタイトルと最上部の日付を編集 */

  // 表示用の日付
  let dateOfMeeting = new Date(calendarDocDataObject.startTime);
  // タイトルを定義して書き換える
  const DOC_TITLE = Utilities.formatDate(dateOfMeeting, 'JST', 'Y/MM') + " 定期MTG";
  newDoc = DocumentApp.openById(calendarDocDataObject.newDocId);
  newDoc.setName(DOC_TITLE);

  // ファイルを取得
  let body = newDoc.getBody();
  // 本文一番上の年月を置換
  const TEMPLATE_HEADLINE = "202X/00";
  body = body.replaceText(TEMPLATE_HEADLINE, Utilities.formatDate(dateOfMeeting, 'JST', 'Y/MM'));
  // テンプレートの日付とミーティング予定日の日付フォーマットを用意
  const TEMPLATE_DATE = "\\*/\\*";
  var headlineDate = Utilities.formatDate(dateOfMeeting, 'JST', 'M/d');

  /** replaceTextで一番上のものを置換する関数 */
  function replaceFirst(old, replacement) {
    // ドキュメントとボディを取得し、テキストを検索    
    var found = body.findText(old);
    if (found) {
      // findTextで見つかったうち、一番上の部分の位置を取得
      let start = found.getStartOffset();
      // 一番下の部分の位置を取得
      let end = found.getEndOffsetInclusive();
      let text = found.getElement().asText();
      text.deleteText(start, end);
      text.insertText(start, replacement);
    }
  }

  // ミーティング予定日と表示用の月が同じ間、文字列置換を続ける
  while (dateOfMeeting.getMonth() == calendarDocDataObject.startTime.getMonth()) {
    replaceFirst(TEMPLATE_DATE, headlineDate);
    // 前のミーティングの分を置換したら、日付を7日分足す
    dateOfMeeting = new Date(calendarDocDataObject.startTime.getFullYear(),
      calendarDocDataObject.startTime.getMonth(), dateOfMeeting.getDate() + 7);
    headlineDate = Utilities.formatDate(dateOfMeeting, 'JST', 'M/d');
  }
}

/**
 * カレンダーを作成する関数
 * @param object calendarDocDataObject カレンダー登録とドキュメント編集に必要なデータ
 * @return void
 */
function CreateSchedule(calendarDocDataObject) {

  /**リモートから参加可能にする場合 */
  if (calendarDocDataObject.attendMeetOrNot == "ON") {
    // イベントに登録するための一連のデータ
    let eventParam = {
      // ミーティングそのもののデータ
      conferenceData: {
        createRequest: {
          conferenceSolutionKey: {
            type: "hangoutsMeet"
          },
          requestId: calendarDocDataObject.meetUrl
        }
      },
      // タイトル、開始時間、終了時間、備考
      summary: calendarDocDataObject.eventTitle,
      "start": {
        "dateTime": calendarDocDataObject.startTimeForMeet,
        "timeZone": "Asia/Tokyo"
      },
      "end": {
        "dateTime": calendarDocDataObject.endTimeForMeet,
        "timeZone": "Asia/Tokyo"
      },
      recurrence: calendarDocDataObject.reccurenceForMeet,
      description: calendarDocDataObject.docAndRemark
    }
    //CalendarAPIに対し、Meet会議付き予定を追加
    Calendar.Events.insert(eventParam, calendarDocDataObject.calendarId, { conferenceDataVersion: 1 });
  }
  /**リモートから参加可能にしない場合 */
  else if (calendarDocDataObject.attendMeetOrNot == "OFF") {
    // セットした値で予定作成
    calendarDocDataObject.calendar.createEventSeries(
      calendarDocDataObject.eventTitle,
      calendarDocDataObject.startTime,
      calendarDocDataObject.endTime,
      calendarDocDataObject.recurence,
      { description: calendarDocDataObject.docAndRemark }
    );
  }
}

あとはGASを開いた時の画面からトリガーを設定すれば、月1で処理が走るようになります。

ただ、これだと「毎週〇曜日」以外の、例えば「隔週水曜日」「第2火曜日」などの細かい設定ができないので、そういう設定もできるようにまた改善していきたいと思います。。

0
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
0
2