├── LICENSE ├── google-apps-script ├── createForm.gs └── vacation.gs └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 vacationhq 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /google-apps-script/createForm.gs: -------------------------------------------------------------------------------- 1 | function createForm(){ 2 | // Create a new form 3 | var form = FormApp.create('AskforLeaveForm'); 4 | var ss = SpreadsheetApp.getActiveSpreadsheet(); 5 | 6 | // Update the form's response destination. 7 | form.setDestination(FormApp.DestinationType.SPREADSHEET, ss.getId()); 8 | 9 | // Remove the sheet "data" because a new response sheet will be created while creating a new form. 10 | var sheet = ss.getSheetByName("data"); 11 | ss.deleteSheet(sheet); 12 | 13 | // Modify the response sheet's name 14 | ss.getSheets()[0].setName("data"); 15 | form.setTitle('請假申請單 Ask for Leave') 16 | .setDescription('這邊可以填一堆請假的注意事項。例如這樣:\n\n1. 告知部門主管,並獲得同意(如「我這週三下午要請半天假,跟貓咪出去玩」)\n2. 填寫請假申請單\n3. 用信件通知大家') 17 | 18 | form.addPageBreakItem().setTitle('選擇假別 Vacation Type'); 19 | 20 | form.addTextItem() 21 | .setTitle('你的 Email') 22 | .setRequired(true); 23 | form.addListItem() 24 | .setTitle('假別 Vacation Type') 25 | .setChoiceValues(['事假 Personal Leave', '病假 Sick Leave', '婚假 Marriage Leave', '喪假 Funeral Leave', '生理假 Menstruation Leave', '公假 Official Leave', '產假 Maternity Leave', '陪產假 Patermity Leave', '彈性休假 Flexible time off']) 26 | .setRequired(true); 27 | form.addDateItem() 28 | .setTitle('開始日期 Multi-date: Begin Date') 29 | .setHelpText('e.g. 2016/04/01 10:00') 30 | .setRequired(true); 31 | form.addListItem() 32 | .setTitle('請假天數 How many days') 33 | .setHelpText('0.5 天可以是上半天、下半天、主管同意的時間') 34 | .setChoiceValues([0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5]) 35 | .setRequired(true); 36 | 37 | Logger.log('Published URL: ' + form.getPublishedUrl()); 38 | Logger.log('Editor URL: ' + form.getEditUrl()); 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AskForLeave 2 | 3 | AskForLeave is a simple vacation tool for small startups. It is done by Google Form + Spreadsheets with lots of formulas. You can add Google Apps Scripts to integrate with Google Calendar, Slack, ... 4 | 5 | ## What does it do? 6 | 7 | 8 | ## Install 9 | 10 | 1. Copy the sample spreadsheet: [ask-for-leave template](https://docs.google.com/spreadsheet/ccc?key=1278CFK8m2w6NY4JN9T4GfSPsVxlGi1P4W2Ndo6cQlC8&newcopy=true) 11 | 2. Create a form like this: http://goo.gl/forms/PPZLpVOdHP , screenshot: https://www.dropbox.com/s/gbwv3rhumiirzuk/google-form-setting.png?dl=0 12 | 3. Link the form to our copied spreadsheet. There will be a newly created sheet in your spreadsheet. You **MUST** set the sheet name into your `vacation.gs` 13 | 4. From the menu in the spreadsheet, click **Tools** > **Script Editor...** 14 | 5. In the script editor, patch the **vacation** code using the updated in `vacation.gs` (Include all correct configurations. For more detail, see [below](#configurations).) 15 | 6. From the menu in the script editor, choose **Resources** > **Current project's triggers** and add the script **vacation** as a trigger of form submission. 16 | 17 | ### Configurations 18 | 19 | All configurations are stored on the top of `vacation.gs` 20 | 21 | * For Google Calendar integration, setting `calendarID` 22 | * For Slack integration, setting both `slackURL` and `slackChannel` 23 | 24 | ## Can I import my existing data? 25 | 26 | (WIP) Yes, you can create a sheet called `imported`, and use the same data format of the `data` sheet. 27 | 28 | ## Contributing 29 | 30 | * Aki Chiu 31 | * Even Wu for the logo of [dayoff bot](https://github.com/vacationhq/hubot-dayoff) 32 | * Liang-Bin Hsueh 33 | * Mosky 34 | * Paris Tsai 35 | * Vikky Huang for the logo of [VacationHQ](https://github.com/vacationhq) organization 36 | * Yuren Ju 37 | 38 | ## Licensing 39 | 40 | MIT License. 41 | 42 | -------------------------------------------------------------------------------- /google-apps-script/vacation.gs: -------------------------------------------------------------------------------- 1 | var sheetName = ''; // the sheet which is linked to your form submission. 2 | var calendarID = ''; 3 | var slackURL = ''; 4 | var slackChannel = ''; 5 | 6 | var vacationDataParser = function(data, row) { 7 | var vacation = { 8 | row: row, 9 | timestamp: data.getRange(row, 1).getValue(), 10 | user: data.getRange(row, 2).getValue(), 11 | type: data.getRange(row, 3).getValue(), 12 | beginDate: data.getRange(row, 4).getValue(), 13 | days: data.getRange(row, 5).getValue(), 14 | dirtyFlag: data.getRange(row, 6).getValue(), 15 | endDate: "", 16 | Date: "" 17 | }; 18 | if (typeof vacation.days !== "number") { 19 | return null; 20 | } 21 | 22 | if (vacation.days <= 0) { 23 | return null; 24 | } 25 | 26 | if (vacation.days <= 1) { 27 | vacation.Date = Utilities.formatDate(vacation.beginDate, 'GMT+8', 'yyyy-MM-dd'); 28 | } 29 | else { 30 | vacation.endDate = new Date(new Date(vacation.beginDate).getTime() + Math.ceil(vacation.days) * (24*3600*1000)); 31 | vacation.Date = Utilities.formatDate(vacation.beginDate, 'GMT+8', 'yyyy-MM-dd') + " - " + Utilities.formatDate(vacation.endDate, 'GMT+8', 'yyyy-MM-dd'); 32 | } 33 | 34 | return vacation; 35 | } 36 | 37 | var addEventToCalendar = function(calendarID, vacation) { 38 | if (calendarID == '') { 39 | return; 40 | } 41 | var calendar = CalendarApp.getCalendarById(calendarID); 42 | 43 | if (vacation.days <= 1) { 44 | calendar.createAllDayEvent(vacation.user + " " + vacation.type, vacation.beginDate); 45 | } 46 | else { 47 | var event = { 48 | summary: vacation.user + " " + vacation.type, 49 | start: { 50 | date: Utilities.formatDate(vacation.beginDate, 'GMT+8', 'yyyy-MM-dd') 51 | }, 52 | end: { 53 | date: Utilities.formatDate(vacation.endDate, 'GMT+8', 'yyyy-MM-dd') 54 | } 55 | }; 56 | 57 | event = Calendar.Events.insert(event, calendarID); 58 | } 59 | } 60 | 61 | var sendEventToSlack = function(slackURL, slackChannel, vacation) { 62 | if (slackURL == '' || slackChannel == '') { 63 | return; 64 | } 65 | 66 | // prepare slack message 67 | var payload = { 68 | "channel": slackChannel, 69 | "username": "請假小幫手", 70 | "icon_emoji": ":memo:", 71 | "link_names": 1, 72 | "attachments":[ 73 | { 74 | "fallback": "This is an update from a Slackbot integrated into your organization. Your client chose not to show the attachment.", 75 | "pretext": "我們剛剛收到一封請假通知......", 76 | "mrkdwn_in": ["pretext"], 77 | "color": "#A6E061", 78 | "fields":[ 79 | { 80 | "title": "員工信箱 Employee Email", 81 | "value": vacation.user, 82 | "short": false 83 | }, 84 | { 85 | "title": "假別 Vacation Type", 86 | "value": vacation.type, 87 | "short": false 88 | }, 89 | { 90 | "title": "日期 Vacation Date", 91 | "value": vacation.Date + " (" + vacation.days + (vacation.days <= 1 ? " day)" : " days)"), 92 | "short": false 93 | } 94 | ] 95 | } 96 | ] 97 | }; 98 | 99 | var options = { 100 | 'method': 'post', 101 | 'payload': JSON.stringify(payload) 102 | }; 103 | 104 | UrlFetchApp.fetch(slackURL, options); 105 | } 106 | 107 | function vacation() { 108 | /* 109 | * trigger when someone submits the form 110 | * 1. add an event to calendar 111 | * 2. send a message to slack 112 | */ 113 | 114 | var data = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName); 115 | 116 | var lastRow = data.getLastRow(); 117 | 118 | for (var row = lastRow; row >= 2; row--) { 119 | var vacation = vacationDataParser(data, row); 120 | if (vacation && vacation.dirtyFlag != true) { 121 | addEventToCalendar(calendarID, vacation); 122 | sendEventToSlack(slackURL, slackChannel, vacation); 123 | data.getRange(vacation.row, 6).setValue(true); 124 | Logger.log("Trigger vacation by " + vacation.user + " in row " + vacation.row); 125 | } 126 | else { 127 | Logger.log("Done"); 128 | break; 129 | } 130 | } 131 | } 132 | --------------------------------------------------------------------------------