スプレッドシートから取得した内容に沿って、議事録テンプレート(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火曜日」などの細かい設定ができないので、そういう設定もできるようにまた改善していきたいと思います。。