コーディング レベル: 初級
所要時間: 15 分
プロジェクトの種類: カスタム メニューを使用した自動化
目標
- ソリューションの機能を理解する
- コンテナ内で Apps Script サービスが 説明します。
- 環境を設定する。
- スクリプトを設定します。
- スクリプトを実行します。
このソリューションについて
お客様のプロジェクトに費やした時間を追跡します。録音 プロジェクト関連の時間を Google カレンダーで作成し、Google スプレッドシートと同期して タイムシートを作成するか、アクティビティを別のタイムシート管理にインポートします ありません時間は、お客様、プロジェクト、タスク別に分類できます。
仕組み
このスクリプトは、同期するカレンダー、カレンダー、スプレッドシートを 同期対象の期間、予定のタイトルと スプレッドシートに入力された情報を含む説明。それらの設定が イベントを同期して、ダッシュボードでアクティビティを表示できます。
スクリプトにより、指定したカレンダーと期間から予定が取り込まれます。 カレンダー。お客様、プロジェクト、パートナーを タスクを category シートを参照し、それに応じて hours シートでイベントにタグを付けます。 こうすることで、ダッシュボード シートを表示したときに、 プロジェクト、タスクの 3 つがあります。
Apps Script サービス
このソリューションでは、次のサービスを使用します。
- HTML サービス - 同期設定を構成できます
- プロパティ サービス - 設定を保存します。 ユーザーがサイドバーで選択した内容です
- カレンダー サービス - カレンダーの招待状が スプレッドシートに追加します。
- スプレッドシート サービス - イベントを書き込みます。 スプレッドシートに送信され、構成されている場合は、更新されたタイトルと説明が送信されます カレンダーに情報を追加します。
前提条件
このサンプルを使用するには、次の前提条件を満たす必要があります。
- Google アカウント(Google Workspace アカウントは 管理者の承認が必要です)。
- インターネットにアクセスできるウェブブラウザ。
環境の設定
既存のカレンダーを使用する場合は、この手順を省略できます。
- calendar.google.com を開きます。
- [他のカレンダー] の横にある「他のカレンダーを追加」アイコン add をクリックします。 > [新しいカレンダーを作成] をタップします。
- カレンダーに名前を付け、[カレンダーを作成] をクリックします。
- カレンダーに予定を追加する。
スクリプトを設定する
次のボタンをクリックして、記録した時間とアクティビティのコピーを作成します。
サンプル スプレッドシート。これに対応する Apps Script プロジェクトでは、
ソリューションが
表示されます。
コピーを作成
スクリプトを実行する
カレンダーの予定を同期する
- [myTime] > [myTime] をクリックします。もしかしたら、 このカスタムメニューを表示するには、ページを更新する必要があります。
プロンプトが表示されたら、スクリプトを承認します。 OAuth 同意画面に [このアプリは確認されていません] という警告が表示された場合は、 [詳細設定] > を選択して続行します {プロジェクト名}(安全でない)に移動します。
もう一度 [myTime] > [myTime] をクリックします。
使用可能なカレンダーのリストから、作成したカレンダーを選択し、 クリックします。
残りの設定を行い、[保存] をクリックします。
myTime をクリックします。> カレンダーを同期します できます。
ダッシュボードを設定する
- [カテゴリ] シートに移動します。
- 顧客、プロジェクト、タスクを追加します。
- [Hours] シートに移動します。
- 同期する予定ごとに、お客様、プロジェクト、タスクを選択します。
- [Dashboard] シートに移動します。
- 最初のセクションには、1 日の合計が表示されます。日付のリストを更新するには、
毎日の合計を計算するには、セル
A1
の日付を変更してください。 - 次のセクションでは、週ごとの合計と、その日付に対応する
A1
で選択されました。 - 最後の 3 つのセクションでは、タスク、プロジェクト、プロジェクトごとの 提供します。
- 最初のセクションには、1 日の合計が表示されます。日付のリストを更新するには、
毎日の合計を計算するには、セル
コードを確認する
このソリューションの Apps Script コードを確認するには、 以下のソースコードを表示します。
ソースコードを表示
コード.gs
// To learn how to use this script, refer to the documentation: // https://developers.google.com/apps-script/samples/automations/calendar-timesheet /* Copyright 2022 Jasper Duizendstra Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ /** * Runs when the spreadsheet is opened and adds the menu options * to the spreadsheet menu */ const onOpen = () => { SpreadsheetApp.getUi() .createMenu('myTime') .addItem('Sync calendar events', 'run') .addItem('Settings', 'settings') .addToUi(); }; /** * Opens the sidebar */ const settings = () => { const html = HtmlService.createHtmlOutputFromFile('Page') .setTitle('Settings'); SpreadsheetApp.getUi().showSidebar(html); }; /** * returns the settings from the script properties */ const getSettings = () => { const settings = {}; // get the current settings const savedCalendarSettings = JSON.parse(PropertiesService.getScriptProperties().getProperty('calendar') || '[]'); // get the primary calendar const primaryCalendar = CalendarApp.getAllCalendars() .filter((cal) => cal.isMyPrimaryCalendar()) .map((cal) => ({ name: 'Primary calendar', id: cal.getId() })); // get the secondary calendars const secundaryCalendars = CalendarApp.getAllCalendars() .filter((cal) => cal.isOwnedByMe() && !cal.isMyPrimaryCalendar()) .map((cal) => ({ name: cal.getName(), id: cal.getId() })); // the current available calendars const availableCalendars = primaryCalendar.concat(secundaryCalendars); // find any calendars that were removed const unavailebleCalendars = []; savedCalendarSettings.forEach((savedCalendarSetting) => { if (!availableCalendars.find((availableCalendar) => availableCalendar.id === savedCalendarSetting.id)) { unavailebleCalendars.push(savedCalendarSetting); } }); // map the current settings to the available calendars const calendarSettings = availableCalendars.map((availableCalendar) => { if (savedCalendarSettings.find((savedCalendar) => savedCalendar.id === availableCalendar.id)) { availableCalendar.sync = true; } return availableCalendar; }); // add the calendar settings to the settings settings.calendarSettings = calendarSettings; const savedFrom = PropertiesService.getScriptProperties().getProperty('syncFrom'); settings.syncFrom = savedFrom; const savedTo = PropertiesService.getScriptProperties().getProperty('syncTo'); settings.syncTo = savedTo; const savedIsUpdateTitle = PropertiesService.getScriptProperties().getProperty('isUpdateTitle') === 'true'; settings.isUpdateCalendarItemTitle = savedIsUpdateTitle; const savedIsUseCategoriesAsCalendarItemTitle = PropertiesService.getScriptProperties().getProperty('isUseCategoriesAsCalendarItemTitle') === 'true'; settings.isUseCategoriesAsCalendarItemTitle = savedIsUseCategoriesAsCalendarItemTitle; const savedIsUpdateDescription = PropertiesService.getScriptProperties().getProperty('isUpdateDescription') === 'true'; settings.isUpdateCalendarItemDescription = savedIsUpdateDescription; return settings; }; /** * Saves the settings from the sidebar */ const saveSettings = (settings) => { PropertiesService.getScriptProperties().setProperty('calendar', JSON.stringify(settings.calendarSettings)); PropertiesService.getScriptProperties().setProperty('syncFrom', settings.syncFrom); PropertiesService.getScriptProperties().setProperty('syncTo', settings.syncTo); PropertiesService.getScriptProperties().setProperty('isUpdateTitle', settings.isUpdateCalendarItemTitle); PropertiesService.getScriptProperties().setProperty('isUseCategoriesAsCalendarItemTitle', settings.isUseCategoriesAsCalendarItemTitle); PropertiesService.getScriptProperties().setProperty('isUpdateDescription', settings.isUpdateCalendarItemDescription); return 'Settings saved'; }; /** * Builds the myTime object and runs the synchronisation */ const run = () => { 'use strict'; myTime({ mainSpreadsheetId: SpreadsheetApp.getActiveSpreadsheet().getId(), }).run(); }; /** * The main function used for the synchronisation * @param {Object} par The main parameter object. * @return {Object} The myTime Object. */ const myTime = (par) => { 'use strict'; /** * Format the sheet */ const formatSheet = () => { // sort decending on start date hourSheet.sort(3, false); // hide the technical columns hourSheet.hideColumns(1, 2); // remove any extra rows if (hourSheet.getLastRow() > 1 && hourSheet.getLastRow() < hourSheet.getMaxRows()) { hourSheet.deleteRows(hourSheet.getLastRow() + 1, hourSheet.getMaxRows() - hourSheet.getLastRow()); } // set the validation for the customers let rule = SpreadsheetApp.newDataValidation() .requireValueInRange(categoriesSheet.getRange('A2:A'), true) .setAllowInvalid(true) .build(); hourSheet.getRange('I2:I').setDataValidation(rule); // set the validation for the projects rule = SpreadsheetApp.newDataValidation() .requireValueInRange(categoriesSheet.getRange('B2:B'), true) .setAllowInvalid(true) .build(); hourSheet.getRange('J2:J').setDataValidation(rule); // set the validation for the tsaks rule = SpreadsheetApp.newDataValidation() .requireValueInRange(categoriesSheet.getRange('C2:C'), true) .setAllowInvalid(true) .build(); hourSheet.getRange('K2:K').setDataValidation(rule); if(isUseCategoriesAsCalendarItemTitle) { hourSheet.getRange('L2:L').setFormulaR1C1('IF(OR(R[0]C[-3]="tbd";R[0]C[-2]="tbd";R[0]C[-1]="tbd");""; CONCATENATE(R[0]C[-3];"|";R[0]C[-2];"|";R[0]C[-1];"|"))'); } // set the hours, month, week and number collumns hourSheet.getRange('P2:P').setFormulaR1C1('=IF(R[0]C[-12]="";"";R[0]C[-12]-R[0]C[-13])'); hourSheet.getRange('Q2:Q').setFormulaR1C1('=IF(R[0]C[-13]="";"";month(R[0]C[-13]))'); hourSheet.getRange('R2:R').setFormulaR1C1('=IF(R[0]C[-14]="";"";WEEKNUM(R[0]C[-14];2))'); hourSheet.getRange('S2:S').setFormulaR1C1('=R[0]C[-3]'); }; /** * Activate the synchronisation */ function run() { console.log('Started processing hours.'); const processCalendar = (setting) => { SpreadsheetApp.flush(); // current calendar info const calendarName = setting.name; const calendarId = setting.id; console.log(`processing ${calendarName} with the id ${calendarId} from ${syncStartDate} to ${syncEndDate}`); // get the calendar const calendar = CalendarApp.getCalendarById(calendarId); // get the calendar events and create lookups const events = calendar.getEvents(syncStartDate, syncEndDate); const eventsLookup = events.reduce((jsn, event) => { jsn[event.getId()] = event; return jsn; }, {}); // get the sheet events and create lookups const existingEvents = hourSheet.getDataRange().getValues().slice(1); const existingEventsLookUp = existingEvents.reduce((jsn, row, index) => { if (row[0] !== calendarId) { return jsn; } jsn[row[1]] = { event: row, row: index + 2 }; return jsn; }, {}); // handle a calendar event const handleEvent = (event) => { const eventId = event.getId(); // new event if (!existingEventsLookUp[eventId]) { hourSheet.appendRow([ calendarId, eventId, event.getStartTime(), event.getEndTime(), calendarName, event.getCreators().join(','), event.getTitle(), event.getDescription(), event.getTag('Client') || 'tbd', event.getTag('Project') || 'tbd', event.getTag('Task') || 'tbd', (isUpdateCalendarItemTitle) ? '' : event.getTitle(), (isUpdateCalendarItemDescription) ? '' : event.getDescription(), event.getGuestList().map((guest) => guest.getEmail()).join(','), event.getLocation(), undefined, undefined, undefined, undefined ]); return true; } // existing event const exisitingEvent = existingEventsLookUp[eventId].event; const exisitingEventRow = existingEventsLookUp[eventId].row; if (event.getStartTime() - exisitingEvent[startTimeColumn - 1] !== 0) { hourSheet.getRange(exisitingEventRow, startTimeColumn).setValue(event.getStartTime()); } if (event.getEndTime() - exisitingEvent[endTimeColumn - 1] !== 0) { hourSheet.getRange(exisitingEventRow, endTimeColumn).setValue(event.getEndTime()); } if (event.getCreators().join(',') !== exisitingEvent[creatorsColumn - 1]) { hourSheet.getRange(exisitingEventRow, creatorsColumn).setValue(event.getCreators()[0]); } if (event.getGuestList().map((guest) => guest.getEmail()).join(',') !== exisitingEvent[guestListColumn - 1]) { hourSheet.getRange(exisitingEventRow, guestListColumn).setValue(event.getGuestList().map((guest) => guest.getEmail()).join(',')); } if (event.getLocation() !== exisitingEvent[locationColumn - 1]) { hourSheet.getRange(exisitingEventRow, locationColumn).setValue(event.getLocation()); } if(event.getTitle() !== exisitingEvent[titleColumn - 1]) { if(!isUpdateCalendarItemTitle) { hourSheet.getRange(exisitingEventRow, titleColumn).setValue(event.getTitle()); } if(isUpdateCalendarItemTitle) { event.setTitle(exisitingEvent[titleColumn - 1]); } } if(event.getDescription() !== exisitingEvent[descriptionColumn - 1]) { if(!isUpdateCalendarItemDescription) { hourSheet.getRange(exisitingEventRow, descriptionColumn).setValue(event.getDescription()); } if(isUpdateCalendarItemDescription) { event.setDescription(exisitingEvent[descriptionColumn - 1]); } } return true; }; // process each event for the calendar events.every(handleEvent); // remove any events in the sheet that are not in de calendar existingEvents.every((event, index) => { if (event[0] !== calendarId) { return true; }; if (eventsLookup[event[1]]) { return true; } if (event[3] < syncStartDate) { return true; } hourSheet.getRange(index + 2, 1, 1, 20).clear(); return true; }); return true; }; // process the calendars settings.calendarSettings.filter((calenderSetting) => calenderSetting.sync === true).every(processCalendar); formatSheet(); SpreadsheetApp.setActiveSheet(hourSheet); console.log('Finished processing hours.'); } const mainSpreadSheetId = par.mainSpreadsheetId; const mainSpreadsheet = SpreadsheetApp.openById(mainSpreadSheetId); const hourSheet = mainSpreadsheet.getSheetByName('Hours'); const categoriesSheet = mainSpreadsheet.getSheetByName('Categories'); const settings = getSettings(); const syncStartDate = new Date(); syncStartDate.setDate(syncStartDate.getDate() - Number(settings.syncFrom)); const syncEndDate = new Date(); syncEndDate.setDate(syncEndDate.getDate() + Number(settings.syncTo)); const isUpdateCalendarItemTitle = settings.isUpdateCalendarItemTitle; const isUseCategoriesAsCalendarItemTitle = settings.isUseCategoriesAsCalendarItemTitle; const isUpdateCalendarItemDescription = settings.isUpdateCalendarItemDescription; const startTimeColumn = 3; const endTimeColumn = 4; const creatorsColumn = 6; const originalTitleColumn = 7; const originalDescriptionColumn = 8; const clientColumn = 9; const projectColumn = 10; const taskColumn = 11; const titleColumn = 12; const descriptionColumn = 13; const guestListColumn = 14; const locationColumn = 15; return Object.freeze({ run: run, }); };
Page.html
<!DOCTYPE html> <!-- Copyright 2022 Google LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> <html> <head> <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css"> <style> #main { display: none } #categories-as-item-title { display: none } #show_title_warning { display: none } #show_description_warning { display: none } .red { color: red; } .branding-below { bottom: 56px; top: 0; } input[type=number] { width: 50px; height: 15px; } </style> </head> <body> <div class="sidebar branding-below" id="wait"> Please wait... </div> <div class="sidebar branding-below" id="main"> <div class="block" id="checks"> <b>Synchronise calendars</b> <div> <span class="error" id="calendar-message"></span> </div> </div> <div class="block"> <b>Synchronisation period</b> <br>Synchronise from the last <input type="number" name="sync-from" id="sync-from"> days <br>Synchronise up to the coming <input type="number" name="sync-to" id="sync-to"> days </div> <div class="block"> <b>Update the calendar items</b><br> <input type="checkbox" id="is-update-calendar-item-title"> <label for="is-update-calendar-item-title">Overwrite the calendar item title</label> <span class="secondary" id="show_title_warning">The calendar title will be overwritten with the values in title column of the sheet</span> </div> <div id="categories-as-item-title"> <input type="checkbox" id="is-use-categories-as-item-title"> <label for="is-use-categories-as-item-title">Use categories as the calendar item title</label> </div> <div class="block"> <input type="checkbox" id="is-update-calendar-item-description"> <label for="is-update-calendar-item-description">Overwrite the calendar item description</label> <span class="secondary" id="show_description_warning">The calendar description will be overwritten with the values in description column of the sheet</span> </div> <div class="block"> <button class="blue" onClick="saveSettings()">Save</button> </div> <div class="block"> <span class="error" id="generic-error"></span> <span class="gray" id="generic-message"></span> </div> </div> <div class="sidebar bottom"> <span class="gray"> myTime v1.2.0</span> </div> </body> <script> // event handler for categrories document.getElementById('is-update-calendar-item-title').addEventListener('change', (event) => { if (event.target.checked) { document.getElementById('categories-as-item-title').style.display = "block"; document.getElementById('show_title_warning').style.display = "block"; } else { document.getElementById('categories-as-item-title').style.display = "none"; document.getElementById('is-use-categories-as-item-title').checked = false; document.getElementById('show_title_warning').style.display = "none"; } }) document.getElementById('is-update-calendar-item-description').addEventListener('change', (event) => { if (event.target.checked) { document.getElementById('show_description_warning').style.display = "block"; } else { document.getElementById('show_description_warning').style.display = "none"; } }) // generic error handler const onFailure = (error) => { console.debug(error); document.getElementById('generic-error').innerHTML = error.message; } // receiving the settings const onSuccessGetSettings = (settings) => { console.debug(settings); settings.calendarSettings.forEach((calendar, index) => { const div = document.createElement('div'); const check = document.createElement('input'); check.className = 'calendar-check'; check.className = 'calendar-check red'; check.type = 'checkbox'; check.id = 'calendar' + index; check.value = (calendar.id); check.name = (calendar.name); check.checked = (calendar.sync); const label = document.createElement('label') label.htmlFor = "calendar" + index; label.appendChild(document.createTextNode(calendar.name)); if (index == 0) { label.className = 'red'; } div.appendChild(check); div.appendChild(label); document.getElementById('checks').appendChild(div); }); document.getElementById('sync-from').value = settings.syncFrom || 31; document.getElementById('sync-to').value = settings.syncTo || 31; document.getElementById('is-update-calendar-item-title').checked = settings.isUpdateCalendarItemTitle; if (settings.isUpdateCalendarItemTitle) { document.getElementById('categories-as-item-title').style.display = "block"; document.getElementById('is-use-categories-as-item-title').checked = settings.isUseCategoriesAsCalendarItemTitle; document.getElementById('show_title_warning').style.display = "block"; } if (settings.isUpdateCalendarItemDescription) { document.getElementById('is-update-calendar-item-description').checked = settings.isUpdateCalendarItemDescription; document.getElementById('show_description_warning').style.display = "block"; } document.getElementById('wait').style.display = "none"; document.getElementById('main').style.display = "block"; } // receiving the settings saved confirmation const onSuccessSaveSettings = (msg) => { console.debug(msg); document.getElementById('generic-message').innerHTML = msg; } // save the settings const saveSettings = () => { document.getElementById('generic-message').innerHTML = ''; const checks = document.getElementsByClassName('calendar-check'); const calendarSettings = []; for (let check of checks) { if (!check.checked) { continue; } calendarSettings.push({ name: check.name, id: check.value, sync: check.checked }); } const settings = {}; settings.calendarSettings = calendarSettings; settings.syncFrom = document.getElementById('sync-from').value; settings.syncTo = document.getElementById('sync-to').value; settings.isUpdateCalendarItemTitle = document.getElementById('is-update-calendar-item-title').checked; if (settings.isUpdateCalendarItemTitle) { settings.isUseCategoriesAsCalendarItemTitle = document.getElementById('is-use-categories-as-item-title').checked; } if (!settings.isUpdateCalendarItemTitle) { settings.isUseCategoriesAsCalendarItemTitle = false; } settings.isUpdateCalendarItemDescription = document.getElementById('is-update-calendar-item-description').checked; console.debug(settings) google.script.run .withFailureHandler(onFailure) .withSuccessHandler(onSuccessSaveSettings) .saveSettings(settings); } // get the initial settings google.script.run .withFailureHandler(onFailure) .withSuccessHandler(onSuccessGetSettings) .getSettings(); </script> </html>
寄稿者
このサンプルは、Google Cloud アーキテクトの Jasper Duizendstra 氏と、 デベロッパー エキスパートX(旧 Twitter)@Duizendstra で Jasper を見つけてください。
このサンプルは、Google Developer Experts のサポートのもと、Google によって管理されています。