はじめに
タスク管理ってみなさんどうされてますか?
わたしはずっと悩んでいて、いろいろアプリを使っていましたが、ピンとくるものがなく…
パソコンがmacでスマホがiphoneということもあり、最近は純正のリマインダーアプリに落ち着いていました。
しかし、生活のタスクと勉強のタスクをリマインダーの日時指定で管理すると、重要な生活のタスクが埋もれてしまうという問題がありました。
リマインダーは生活のタスク用にして、勉強のタスクを管理する別のアプリでいいものはないかな…
求めるものとしては次のとおりでした。
- チェックリスト形式になっている
- 勉強時間を計測できる
- 勉強の記録がログとして保存できる(できたらスケジュールに載せたい)
1,2はそれぞれ実現できるアプリはあるのですが、両方備えたものが存在しませんでした。
3については、純正リマインダーを使うとスケジュールアプリMocaにその日期限のリマインダーを表示させることができるのですが、タスクを完了してしまうと表示が消えてしまいます。
1,2,3すべてを兼ね備えたアプリを見つけられなかったので、GoogleスプレッドシートのToDoテンプレートをベースにして、GAS (Google Apps Script)というローコードツールでプログラミングしてアプリを自作することにしました。
GAS (Google Apps Script)とは?
公式ページから引用します。
Apps Script は、Google Workspace の統合、自動化、拡張のためのビジネス ソリューションをすばやく簡単に構築するための唯一のローコード プラットフォームです。Apps Script を使えば、ビジネス ユーザーは本格的なソフトウェア開発の経験がなくても、Google Workspace 上にカスタム ソリューションを構築できます。Apps Script は、Gmail アカウントをお持ちであればどなたでもご利用いただけます。
https://workspace.google.co.jp/intl/ja/products/apps-script/
簡単にいうと、スプレッドシート、ドキュメント、スライド、カレンダー、メール、フォームなどのGoogleのアプリケーションにおいて、プログラムを書くことで連携させたり、トリガーで自動化したり、自分の望む処理を実現したりできるプラットフォームのことです。
GASはJavaScriptベースの言語ですが、Googleのアプリケーションのための組み込み関数が定義されていて、簡単にコーディングすることができます(JavaScriptの組み込み関数で使えないものもあります)。
カレンダーやメール、スプレッドシートなどの機能を自分で一から作るのは大変ですが、GASを使うと既存のサービスの上に機能を載せることができます。
詳細については公式ページhttps://www.google.com/script/start/や、以下の本を参考にしてください。
『Google Apps Script目的別リファレンス 実践サンプルコード付き 第3版』
用途向けにサンプルコードが載っているので、使い道がイメージしやすいです。
『詳解! Google Apps Script完全入門 [第3版]』
こちらは辞書的に使うのがおすすめ。
GASを使うにはどうしたらいいの?
GASはスタンドアロン型とコンテナバインド型がありますが、今回はスプレッドシートに直接紐づけるコンテナバインド型で実装していきます。
まずはGoogleアカウントにログインして、スプレッドシートを開いてください。
上部タブの「拡張機能」を選択すると、「Apps Script」が出てくるのでこれを選択します。
するとエディタが立ち上がります。
なにか処理を記述するとフロッピーボタン(保存)がアクティブになり、保存できるようになります。最初は保存しないと実行ボタンがアクティブになりません。
最初に実行した時に、Googleから認証を求められます。
詳細はこちらhttps://programming-zero.net/gas-first-run-process/ をご覧ください。
ストップウオッチつきタスク管理ツールの概要
スプレッドシートのテンプレート「ToDo」をベースにしています。
こちらhttps://www.acrovision.jp/service/gas/?p=269のページを参考に、プログラムを実行するボタンを設置しました。ボタンに対する関数の割り当て方法も書いてあります。
まず、時間を測定したいタスクの右側の「timer check」にチェックを入れる必要があります。
- startボタン:ストップウオッチを開始します。測定時間はC1列に表示されます。
- stopボタン:ストップウオッチを停止します。E列「time」に累計測定時間が表示され、タスク名、スタート時間、ストップ時間でGoogleカレンダーに予定が登録されます。
- archive&colorボタン:A列のチェックボックスにチェックが入っているタスク(完了したタスク)をTaskLogシートに移動します。また、日付に色をつけます。今日は青色、今日より前は赤色、未来のタスクは黒色に変更します。
startボタンを押すと、下図のようにC1セルに測定時間が表示されます。
残念なことにGASの仕様では、1分間のどこかの時点での更新処理しか実現できないため、表示時間は最大で約2分のずれが生じます。timeに表示される累計時間はstop時点の時刻とstart時点の時刻の差で算出しているので、こちらにずれはありません。
stopボタンを押すとストップウオッチが停止し、Googleカレンダーにチェックを入れたタスク名で予定が作成されます。
ポップアップを消さないと処理が終了しません。
このような感じで予定が登録されます。今回は自分で作成した「勉強」カレンダーに予定を登録する設定にしています。
archive&colorボタンを押すと、完了したタスクがTaskLogに移動します。A列とD列はチェックボックスのTrue or Falseなので、非表示にしています。
時間を計測してカレンダーに登録できるようになりました!
勉強がはかどりそうです。
コード
一回だけcalendarIdをプロパティストアに登録する処理を実行する必要があります。
カレンダーIDの確認方法はこちらhttps://blog-and-destroy.com/41932を参考にしました。
// calendarIdというkeyで変数calendarIdをプロパティストアに登録する
var calendarId = "カレンダーアプリから取得したcalendarID";
PropertiesService.getScriptProperties().setProperty("calendarId",calendarId);
実装コードは次のとおりです。
function stopwatch_start() {
//////////////////////////
//startボタンに割り当てる関数//
//ストップウオッチをスタートする//
/////////////////////////
var ui = SpreadsheetApp.getUi();
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("ToDo");
var values = sheet.getRange("D4:D").getValues();
var hasTrue = false;
for (var i = 0; i < values.length; i++) {
if (values[i][0] === true) {
hasTrue = true;
break;
}
}
if (hasTrue) {
// タイマー開始時の時間を取得
var startTime = new Date().getTime();
console.log(startTime);
// グローバル変数として開始時刻を保持
PropertiesService.getScriptProperties().setProperty("startTime", startTime);
console.log(PropertiesService.getScriptProperties().getProperty("startTime"));
// タイマー更新関数を定期的に実行するトリガーを作成
ScriptApp.newTrigger("_updateElapsedTime")
.timeBased()
.everyMinutes(1)
.create();
// C1セルを初期化
sheet.getRange("C1").setValue("0分");
}else{
ui.alert("timer checkにチェックを入れてください。");
}
}
function stopwatch_stop() {
//////////////////////////////
//stopボタンに割り当てる関数///////
//ストップウオッチをストップする///////
//////////////////////////////
var stopTime = new Date().getTime();
// トリガーを削除
var triggers = ScriptApp.getProjectTriggers();
for (var i = 0; i < triggers.length; i++) {
if (triggers[i].getHandlerFunction() === "_updateElapsedTime") {
ScriptApp.deleteTrigger(triggers[i]);
break;
}
}
//タスクの行のE列に累計時間を表示する
_addMinutesToEColumn(stopTime)
//予定を作成
_createCalendarEvents(stopTime)
PropertiesService.getScriptProperties().deleteProperty("startTime");
// C1セルをクリア
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
sheet.getRange("C1").clearContent();
}
function _updateElapsedTime() {
///////////////////////////////////
//経過時間を計算してC1セルに表示する関数//
///////////////////////////////////
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("ToDo");
// 開始時刻を取得
var startTime = PropertiesService.getScriptProperties().getProperty("startTime");
var startTime_int = parseFloat(startTime).toFixed(0);
if (startTime) {
// 現在の時間を取得
var currentTime = new Date().getTime();
// 経過時間を計算
var elapsedTime = currentTime - startTime_int;
// 分を計算
var minutes = Math.floor(elapsedTime / 60000);
// C1セルに時間を表示
sheet.getRange("C1").setValue("measuring : " + minutes + "分");
}
}
function _createCalendarEvents(stopTime) {
////////////////////////////
//カレンダーにタスクを登録する関数//
////////////////////////////
var ui = SpreadsheetApp.getUi();
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = spreadsheet.getSheetByName("ToDo");
var data = sheet.getRange("C4:D").getValues(); // C4からD列のデータを取得
//カレンダーIDをプロパティストアから取得し、カレンダーオブジェクトを生成
var calendarId = PropertiesService.getScriptProperties().getProperty("calendarId");
var calendar = CalendarApp.getCalendarById(calendarId);
//プロパティストアから取得したstartTimeの処理
var startTime = PropertiesService.getScriptProperties().getProperty("startTime");
var startTime_int = parseFloat(startTime).toFixed(0);
var startTime_number = Number(startTime_int);
//カレンダーにタイムゾーンを指定
calendar.setTimeZone("Asia/Tokyo");
for (var i = 0; i < data.length; i++) {
if (data[i][1] === true) {
var task = data[i][0]; // C列の値(task)を取得
if(startTime){
// 予定をカレンダーに登録
var event = calendar.createEvent(task,new Date(startTime_number), new Date(stopTime));
ui.alert("予定が作成されました:" + event.getTitle());
}else{
ui.alert('stopの前にstartを押してください。')
}
}
}
}
function archiveAndColor(){
////////////////////////////////////
//archive & colorボタンに割り当てる関数//
///////////////////////////////////
_moveRowsToTaskLog()
_updateCellColors()
}
function _moveRowsToTaskLog() {
//////////////////////////////////
//完了したタスクをTaskLogに移動する関数//
//////////////////////////////////
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var sourceSheet = spreadsheet.getSheetByName("ToDo");
var targetSheet = spreadsheet.getSheetByName("TaskLog");
var sourceData = sourceSheet.getRange("A:E").getValues(); // AからE列までの値を取得
var targetRange = targetSheet.getRange(targetSheet.getLastRow() + 1, 1, 1, 5); // 移動先のシートの末尾の次の行に移動するための範囲を取得
for (var i = sourceData.length - 1; i >= 0; i--) {
if (sourceData[i][0] === true) {
var rowData = sourceData.splice(i, 1)[0]; // 移動する行のデータを取得して元の配列から削除
targetRange.setValues([rowData.slice(0, 5)]); // 移動先の範囲に値をセット(AからE列まで)
targetRange = targetRange.offset(1, 0); // 次の行に移動するために範囲をオフセット
}
}
// 移動後のデータを元のシートに反映
sourceSheet.getRange(1, 1, sourceData.length, 5).setValues(sourceData);
}
function _addMinutesToEColumn(stopTime) {
//////////////////////////
//E列に累積時間を表示する関数//
//////////////////////////
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = spreadsheet.getSheetByName("ToDo"); // シート名を適宜変更する
var data = sheet.getRange("D:E").getValues(); // D列とE列の値を取得
var outputData = []; // 出力するデータを格納する配列
// 開始時刻を取得
var startTime = PropertiesService.getScriptProperties().getProperty("startTime");
var startTime_int = parseFloat(startTime).toFixed(0);
if (startTime) {
// 経過時間を計算
var elapsedTime = stopTime - startTime_int;
// 分を計算
var minutes = Math.floor(elapsedTime / 60000);
}
for (var i = 0; i < data.length; i++) {
var dValue = data[i][0]; // D列の値
var eValue = data[i][1]; // E列の値
if (dValue === true) {
var newValue = eValue + minutes; // E列の値にminutesを加算
// E列に値が既に入っている場合は加算した値を使用
if (eValue !== "") {
newValue = eValue + minutes;
}
// 出力するデータを配列に追加
outputData.push([newValue]);
} else {
// D列がTrueでない場合はE列の値をそのまま出力
outputData.push([eValue]);
}
}
// 出力データをシートのE列に書き込む
sheet.getRange(1, 5, outputData.length, 1).setValues(outputData);
}
function _updateCellColors() {
//////////////////////////////
//B列の日付の色を変更する関数/////
/////////////////////////////
var ui = SpreadsheetApp.getUi();
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = spreadsheet.getSheetByName("ToDo");
var data = sheet.getRange("B4:B").getValues(); // B列の値を取得
var cell = sheet.getRange("B4:B");
var currentDate = new Date(); // 現在の日時を取得
// 年、月、日を個別に取得
var year1 = currentDate.getFullYear();
var month1 = currentDate.getMonth();
var day1 = currentDate.getDate();
for (var i = 0; i < data.length; i++) {
var cellValue = data[i][0];
if (cellValue === "") {
continue; // セルの値が空の場合はスキップ
}
var cellDate = new Date(cellValue);
var year2 = cellDate.getFullYear();
var month2 = cellDate.getMonth();
var day2 = cellDate.getDate();
if (year1 === year2 && month1 === month2 && day1 === day2) {
cell.getCell(i+1,1).setFontColor("blue"); // 現在の日付と一致する場合は青色にする
} else if (year2 < year1 || (year2 === year1 && month2 < month1) || (year2 === year1 && month2 === month1 && day2 < day1)) {
cell.getCell(i+1,1).setFontColor("red"); // 現在の日付より前の場合は赤色にする
}else{
cell.getCell(i+1,1).setFontColor("black"); // それ以外は黒色にする
}
}
}
実装のポイントとしては
ScriptApp.getProjectTriggers()
を使用して測定時間の更新処理を行なった。- ここで指定する関数は引数を指定できないので、startTimeをプロパティストアに格納した。
- プロパティストアから読み込まれるタイムスタンプstartTimeは指数表示(3.11E12みたいな)になっているので、
parseFloat(startTime).toFixed(0)
で整数表示に変更する。また、Dateオブジェクトにする際にNumber型に変換する必要があるのでNumber()
で変換した。
- スプレッドシートに対する処理は、
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("ToDo")
でシート名「ToDo」のシートを取得し、var data = sheet.getRange("C4:D").getValues();
のようにgetRange()
とgetValues()
,getValue()
でセルの値を取得する。 - ボタンに対する関数の割り当て方法は先ほど紹介したこちらhttps://www.acrovision.jp/service/gas/?p=269のページを参考にした
おわりに
ChatGPTに相談しつつ、半日くらいでアプリをつくることができました。
一からWebアプリケーションを作っていたらもっと大変だったと思います。
Googleのサービスをベースに何か作りたい時にはGAS、便利ですよ〜
コメント