Skip to content

Instantly share code, notes, and snippets.

@naosim
Last active July 31, 2023 14:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save naosim/772ddc29cf6a0d140d6db635dfc3f452 to your computer and use it in GitHub Desktop.
Save naosim/772ddc29cf6a0d140d6db635dfc3f452 to your computer and use it in GitHub Desktop.
spreadsheetでtaskAPIを使ってタスクを管理する
// いい感じに同期します
// 新規タスクの追加
// 変更点の更新
// GoogleTasksの読み込み
function update() {
new MainService().update();
}
function onOpen() {
const customMenu = SpreadsheetApp.getUi()
customMenu.createMenu('タスク')
.addItem('更新', 'update')
.addToUi()
}
// スプレッドシートをセットアップします。
function setupSheet() {
new TaskSheetRepository().setupSheet();
}
class MainService {
constructor() {
this.googleTasksRepository = new GoogleTasksRepository();
this.taskSheetRepository = new TaskSheetRepository();
}
static dateToText(dateOrText) {
if(!dateOrText) {
return dateOrText;
}
if(toString.call(dateOrText).indexOf('Date') == -1) {
return dateOrText;
}
return dateOrText.toLocaleDateString();
}
static convertGoogleTaskToSheetTask(googleTask) {
const short = [googleTask.title, MainService.dateToText(googleTask.dates.due), googleTask.notes].filter(v => v).map(v => v.trim()).join('\n').slice(0, 140);
const result = {
id: googleTask.id,
title: googleTask.title,
notes: googleTask.notes,
due: googleTask.dates.due,
completed: googleTask.dates.completed,
updated: googleTask.dates.updated,
short
}
return result;
}
update() {
// シート上の変更をGoogleTaskに反映する
const diff = this.taskSheetRepository.getDiffTasks();
const newTaskItems = diff.filter(v => v.status == 'new');
const updateTaskItems = diff.filter(v => v.status == 'update');
newTaskItems.forEach(v => {
this.googleTasksRepository.insert(v.data);
})
updateTaskItems.forEach(v => {
this.googleTasksRepository.update(v.id, v.data);
})
// googleからタスクを取得
const googleTasks = this.googleTasksRepository.getTasks();
// sheet用に変換
const sheetTasks = googleTasks.map(v => MainService.convertGoogleTaskToSheetTask(v));
// sheetへ保存
this.taskSheetRepository.updateSheet(sheetTasks);
}
}
class DiffJudge {
judge(s, b) {
const eqDate = (a, b) => {
if(!a && !b) {
return true;
}
if(a && !b) {
return false;
}
if(!a && b) {
return false;
}
return a.getTime() == b.getTime();
};
const result = {status: 'none', id: s.id, data: {}};
if(!s.id) {
return {status: 'new', data: s};
}
if(s.title != b.title) {
result.status = 'update';
result.data.title = s.title;
}
if(s.notes != b.notes) {
result.status = 'update';
result.data.notes = s.notes;
}
if(!eqDate(s.due, b.due)) {
result.status = 'update';
result.data.due = s.due;
}
return result;
}
}
class TaskSheetRepository {
constructor() {
this._sheetName = 'シート1';
this._backupSheetName = 'タスクバックアップ'
this._columns = [
'short',
'completed',
'title',
'notes',
'due',
'id',
'updated',
]
this.spreadSheet = SpreadsheetApp.getActiveSpreadsheet();
}
getSpreadSheet() {
return this.spreadSheet;
}
getSheet() {
if(!this.sheet) {
this.sheet = this.getSpreadSheet().getSheetByName(this._sheetName);
}
return this.sheet;
}
getBackupSheet() {
return this.getSpreadSheet().getSheetByName(this._backupSheetName);
}
setupSheet() {
const sheet = this.getSheet();
sheet.clear();
const range = sheet.getRange(1, 1, 1, this._columns.length);
range.setValues([this._columns]);
const backupSheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet();
backupSheet.setName(this._backupSheetName);
backupSheet.getRange(1, 1, 1, this._columns.length).setValues([this._columns]);
}
updateSheet(sheetTasks) {
const table = [
this._columns,
...sheetTasks.map(t => this._columns.map(c => t[c]))
];
const updateSheet = (sheet) => {
sheet.clearContents();
const range = sheet.getRange(1, 1, table.length, this._columns.length);
range.setValues(table);
}
updateSheet(this.getBackupSheet());
updateSheet(this.getSheet());
}
getDiffTasks() {
const aryToTask = ary => {
return ary.reduce((memo, v, i) => {
memo[this._columns[i]] = v;
return memo;
}, {})
}
const sheetValues = this.getSheet().getDataRange().getValues().slice(1).map(ary => aryToTask(ary));
const backupValues = this.getBackupSheet().getDataRange().getValues().slice(1).map(ary => aryToTask(ary));
return sheetValues.map((s, i) => {
const b = backupValues[i];
return new DiffJudge().judge(s, b);
}).filter(v => v.status != 'none');
}
}
class GoogleTasksRepository {
constructor() {
this.taskListId = null;
if(!this.taskListId) {
throw new Error('GoogleTasksRepository.taskListIdを設定してください');
}
}
insert(task) {
/*
{
title: string,
notes: string,
due: Date,
}
*/
const input = {
title: task.title,
notes: task.notes
};
if(task.due) {
input.due = Utilities.formatDate(task.due, "Asia/Tokyo", "yyyy-MM-dd") + "T00:00:00.000Z"
}
Tasks.Tasks.insert(input, this.taskListId);
}
update(taskId, task) {
}
getTasks() {
return Tasks.Tasks.list(this.taskListId, {
showCompleted: true,
showHidden: true
}).items.map(v => {
v.dates = {};
// 日付をDate型に変える
const keys = ['updated', 'completed', 'due'];
keys.forEach(k => {
if(v[k]) {
v.dates[k] = new Date(v[k]);
}
})
return v;
});
/* memo
{
"kind": string,
"id": string,
"etag": string,
"title": string,
"updated": string,
"selfLink": string,
"parent": string,
"position": string,
"notes": string,
"status": string,
"due": string,
"completed": string,
"deleted": boolean,
"hidden": boolean,
"links": [
{
"type": string,
"description": string,
"link": string
}
]
}
*/
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment