Cấp độ lập trình: Cơ bản
Thời lượng: 15 phút
Loại dự án: Tự động hoá bằng một trình đơn tuỳ chỉnh
Mục tiêu
- Hiểu rõ chức năng của giải pháp.
- Tìm hiểu chức năng của các dịch vụ Apps Script trong giải pháp này.
- Thiết lập môi trường.
- Thiết lập tập lệnh.
- Chạy tập lệnh.
Thông tin về giải pháp này
Theo dõi thời gian thực hiện dự án cho khách hàng. Bạn có thể ghi lại thời gian liên quan đến dự án vào Lịch Google, sau đó đồng bộ hoá thời gian đó với Google Trang tính để tạo bảng chấm công hoặc nhập hoạt động của bạn vào một hệ thống quản lý bảng chấm công khác. Bạn có thể phân loại thời gian theo khách hàng, dự án và công việc.
Cách thức hoạt động
Tập lệnh cung cấp một thanh bên cho phép bạn chọn lịch cần đồng bộ hoá, khoảng thời gian đồng bộ hoá cũng như việc có ghi đè tiêu đề và nội dung mô tả sự kiện bằng thông tin đã nhập vào bảng tính hay không. Sau khi định cấu hình các chế độ cài đặt đó, bạn có thể đồng bộ hoá sự kiện và xem các hoạt động của mình trên trang tổng quan.
Tập lệnh này đưa các sự kiện từ lịch và khoảng thời gian mà bạn chỉ định từ Lịch vào bảng tính. Bạn có thể thêm khách hàng, dự án và công việc vào trang tính danh mục, sau đó gắn thẻ các sự kiện tương ứng trong trang giờ.
Nhờ đó, khi xem trang tính trang tổng quan, bạn có thể xem tổng thời gian theo khách hàng, dự án và công việc.
Dịch vụ Apps Script
Giải pháp này sử dụng các dịch vụ sau:
- Dịch vụ HTML – Tạo thanh bên dùng để định cấu hình các chế độ cài đặt đồng bộ hoá.
- Dịch vụ thuộc tính – Lưu trữ các chế độ cài đặt mà người dùng chọn trên thanh bên.
- Dịch vụ Lịch – Gửi thông tin sự kiện đến bảng tính.
- Dịch vụ bảng tính – Ghi các sự kiện vào bảng tính và nếu đã định cấu hình, tính năng này sẽ gửi thông tin cập nhật về tiêu đề và nội dung mô tả cho Lịch.
Điều kiện tiên quyết
Để sử dụng mẫu này, bạn cần có các điều kiện tiên quyết sau:
- Tài khoản Google (các tài khoản Google Workspace có thể
phải được quản trị viên phê duyệt).
- Một trình duyệt web có quyền truy cập vào Internet.
Thiết lập môi trường
Nếu định sử dụng lịch hiện có, bạn có thể bỏ qua bước này.
- Truy cập vào calendar.google.com.
- Bên cạnh Lịch khác, hãy nhấp vào biểu tượng Thêm lịch khác add
> Tạo lịch mới.
- Đặt tên cho lịch của bạn rồi nhấp vào Tạo lịch.
- Thêm một số sự kiện vào lịch.
Thiết lập tập lệnh
Nhấp vào nút sau đây để tạo bản sao của bảng tính mẫu Ghi lại thời gian và hoạt động. Dự án Apps Script cho giải pháp này được đính kèm vào bảng tính.
Tạo bản sao
Chạy tập lệnh
Đồng bộ hoá sự kiện trên lịch
- Nhấp vào myTime > Cài đặt. Có thể bạn cần phải làm mới trang để trình đơn tuỳ chỉnh này xuất hiện.
Khi được nhắc, hãy cho phép tập lệnh chạy.
Nếu màn hình xin phép bằng OAuth hiển thị cảnh báo, Ứng dụng này chưa được xác minh,
hãy tiếp tục bằng cách chọn Nâng cao >
Chuyển đến {Project Name} (không an toàn).
Nhấp lại vào myTime > Settings (Cài đặt).
Trong danh sách các lịch có sẵn, hãy chọn lịch bạn đã tạo và mọi lịch khác mà bạn muốn đồng bộ hoá.
Định cấu hình các chế độ cài đặt còn lại rồi nhấp vào Save (Lưu).
Nhấp vào myTime > Đồng bộ hoá sự kiện lịch.
Thiết lập trang tổng quan
- Chuyển đến trang tính Danh mục.
- Thêm khách hàng, dự án và việc cần làm.
- Chuyển đến trang tính Giờ hoạt động.
- Đối với mỗi sự kiện được đồng bộ hoá, hãy chọn khách hàng, dự án và việc cần làm.
- Chuyển đến trang tính Trang tổng quan.
- Phần đầu tiên cung cấp tổng số hàng ngày. Để cập nhật danh sách ngày cho dữ liệu tổng số hằng ngày, hãy thay đổi ngày trong ô
A1
.
- Phần tiếp theo cung cấp dữ liệu tổng số hằng tuần và tương ứng với ngày đã chọn trong
A1
.
- Ba phần cuối cùng cung cấp thông tin tổng thể theo nhiệm vụ, dự án và khách hàng.
Xem xét mã
Để xem lại mã Apps Script cho giải pháp này, hãy nhấp vào phần Xem mã nguồn bên dưới:
Xem mã nguồn
Code.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,
});
};
Người đóng góp
Mẫu này do Jasper Duizendstra, Kiến trúc sư đám mây của Google và Chuyên gia nhà phát triển của Google tạo ra. Tìm Jasper trên Twitter @Duizendstra.
Mẫu này được Google duy trì với sự trợ giúp của các chuyên gia nhà phát triển của Google.
Các bước tiếp theo