diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index f9f71a2..207d653 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -97,4 +97,15 @@ Please say `hi` on your test channel. The bot will create new record on the time That's All! Pull Requests are very welcome! -You can find additional inormation from [Original Repos](https://github.com/masuidrive/miyamoto) \ No newline at end of file +You can find additional inormation from [Original Repos](https://github.com/masuidrive/miyamoto) + + +## Update script + +When you update the Google Apps Script, you have to create new version for enabling the modification for the API. + +1. Run `make dev` and upload the new main.gs. +2. Open your Google Apps script +3. Make a new version from `menu -> manage versions -> save new version` +4. Publish new version from `menu -> Publish -> Deploy as web app` and select the latest version. + diff --git a/main.gs b/main.gs index 5ccc4f2..add577e 100644 --- a/main.gs +++ b/main.gs @@ -65,6 +65,37 @@ loadDateUtils = function () { return null; }; + // テキストから休憩時間を抽出 + DateUtils.parseMinutes = function(str) { + str = String(str || "").toLowerCase().replace(/[A-Za-z0-9]/g, function(s) { + return String.fromCharCode(s.charCodeAt(0) - 0xFEE0); + }); + var reg = /(\d*.\d*\s*(分|minutes?|mins|時間|hour|hours))(\d*(分|minutes?|mins))?/; + var matches = str.match(reg); + if(matches) { + var hour = 0; + var min = 0; + + // 最初のマッチ + if(matches[1] != null) { + if (['時間','hour','hours'].includes(matches[2])) { + // 1.5 時間 + hour = parseFloat(matches[1], 10); + // 2回めのマッチ + if(matches[3] != null) { + min = parseInt(matches[3], 10); + } + } else { + // 60 分 + min = parseInt(matches[1], 10); + } + } + + return [hour * 60 + min]; + } + return null; + }; + // テキストから日付を抽出 DateUtils.parseDate = function(str) { str = String(str || "").toLowerCase().replace(/[A-Za-z0-9]/g, function(s) { @@ -327,12 +358,12 @@ loadGSProperties = function (exports) { var vals = this.sheet.getRange("A1:A"+this.sheet.getLastRow()).getValues(); for(var i = 0; i < this.sheet.getLastRow(); ++i) { if(vals[i][0] == key) { - this.sheet.getRange("C"+(i+1)).setValue(note); + this.sheet.getRange("D"+(i+1)).setValue(note); return; } } } - this.sheet.getRange("A"+(this.sheet.getLastRow()+1)+":C"+(this.sheet.getLastRow()+1)).setValues([[key, '', note]]); + this.sheet.getRange("A"+(this.sheet.getLastRow()+1)+":D"+(this.sheet.getLastRow()+1)).setValues([[key, '', note]]); return; }; @@ -358,18 +389,21 @@ loadGSTemplate = function() { } else { var now = DateUtils.now(); - this.sheet.getRange("A1:L2").setValues([ + this.sheet.getRange("A1:N2").setValues([ [ - "出勤", "出勤更新", "退勤", "退勤更新", "休暇", "休暇取消", - "出勤中", "出勤なし", "休暇中", "休暇なし", "出勤確認", "退勤確認" + "出勤", "出勤更新", "退勤", "退勤更新", "休憩", "休暇", "休暇取消", + "出勤中", "出勤なし", "休暇中", "休暇なし", "出勤確認", "退勤確認", + "休憩エラー" ], [ "<@#1> Good morning (#2)!", "<@#1> I changed starting time to #2", "<@#1> Great work! (#2)", "<@#1> I changed leaving time to #2", + "<@#1> I changed break time to #2", "<@#1> I registered a holiday for #2", "<@#1> I canceled holiday #2", "#1 is working", "All staffs are working", "#2 is having a holiday at #1", "No one is having a holiday at #1", - "Is today holiday? #1", "Did you finish working today? #1" + "Is today holiday? #1", "Did you finish working today? #1", + "[Error] You have not started working today!" ] ]); } @@ -430,6 +464,7 @@ loadGSTimesheets = function () { { name: '日付' }, { name: '出勤' }, { name: '退勤' }, + { name: '休憩(分)' }, { name: 'ノート' }, ], properties: [ @@ -487,17 +522,17 @@ loadGSTimesheets = function () { return v === '' ? undefined : v; }); - return({ user: username, date: row[0], signIn: row[1], signOut: row[2], note: row[3] }); + return({ user: username, date: row[0], signIn: row[1], signOut: row[2], break: row[3], note: row[4] }); }; GSTimesheets.prototype.set = function(username, date, params) { var row = this.get(username, date); - _.extend(row, _.pick(params, 'signIn', 'signOut', 'note')); + _.extend(row, _.pick(params, 'signIn', 'signOut', 'break', 'note')); var sheet = this._getSheet(username); var rowNo = this._getRowNo(username, date); - var data = [DateUtils.toDate(date), row.signIn, row.signOut, row.note].map(function(v) { + var data = [DateUtils.toDate(date), row.signIn, row.signOut, row.break, row.note].map(function(v) { return v == null ? '' : v; }); sheet.getRange("A"+rowNo+":"+String.fromCharCode(65 + this.scheme.columns.length - 1)+rowNo).setValues([data]); @@ -733,6 +768,7 @@ loadTimesheets = function (exports) { // 日付は先に処理しておく this.date = DateUtils.parseDate(message); this.time = DateUtils.parseTime(message); + this.minutes = DateUtils.parseMinutes(message) this.datetime = DateUtils.normalizeDateTime(this.date, this.time); if(this.datetime !== null) { this.dateStr = DateUtils.format("Y/m/d", this.datetime); @@ -744,6 +780,7 @@ loadTimesheets = function (exports) { ['actionSignOut', /(バ[ー〜ァ]*イ|ば[ー〜ぁ]*い|おやすみ|お[つっ]ー|おつ|さらば|お先|お疲|帰|乙|bye|night|(c|see)\s*(u|you)|left|退勤|ごきげんよ|グ[ッ]?バイ)/], ['actionWhoIsOff', /(だれ|誰|who\s*is).*(休|やす(ま|み|む))/], ['actionWhoIsIn', /(だれ|誰|who\s*is)/], + ['actionBreak', /(休憩|break)/], ['actionCancelOff', /(休|やす(ま|み|む)|休暇).*(キャンセル|消|止|やめ|ません)/], ['actionOff', /(休|やす(ま|み|む)|休暇)/], ['actionSignIn', /(モ[ー〜]+ニン|も[ー〜]+にん|おっは|おは|へろ|はろ|ヘロ|ハロ|hi|hello|morning|ohayo|出勤)/], @@ -798,6 +835,21 @@ loadTimesheets = function (exports) { } }; + // 休憩 + Timesheets.prototype.actionBreak = function(username, time) { + if (this.minutes) { + var data = this.storage.get(username, this.datetime); + if(!data.signIn || data.signIn === '-') { + // まだ出勤前である + this.responder.template("休憩エラー", username, "" ); + } else { + // break 入力 + this.storage.set(username, this.datetime, {break: this.minutes}); + this.responder.template("休憩", username, this.minutes + "分"); + } + } + }; + // 休暇申請 Timesheets.prototype.actionOff = function(username, message) { if(this.date) { diff --git a/scripts/date_utils.js b/scripts/date_utils.js index dc239be..0979119 100644 --- a/scripts/date_utils.js +++ b/scripts/date_utils.js @@ -62,6 +62,37 @@ loadDateUtils = function () { return null; }; + // テキストから休憩時間を抽出 + DateUtils.parseMinutes = function(str) { + str = String(str || "").toLowerCase().replace(/[A-Za-z0-9]/g, function(s) { + return String.fromCharCode(s.charCodeAt(0) - 0xFEE0); + }); + var reg = /(\d*.\d*\s*(分|minutes?|mins|時間|hour|hours))(\d*(分|minutes?|mins))?/; + var matches = str.match(reg); + if(matches) { + var hour = 0; + var min = 0; + + // 最初のマッチ + if(matches[1] != null) { + if (['時間','hour','hours'].includes(matches[2])) { + // 1.5 時間 + hour = parseFloat(matches[1], 10); + // 2回めのマッチ + if(matches[3] != null) { + min = parseInt(matches[3], 10); + } + } else { + // 60 分 + min = parseInt(matches[1], 10); + } + } + + return [hour * 60 + min]; + } + return null; + }; + // テキストから日付を抽出 DateUtils.parseDate = function(str) { str = String(str || "").toLowerCase().replace(/[A-Za-z0-9]/g, function(s) { diff --git a/scripts/gs_properties.js b/scripts/gs_properties.js index 2687f8c..dac781c 100644 --- a/scripts/gs_properties.js +++ b/scripts/gs_properties.js @@ -46,12 +46,12 @@ loadGSProperties = function (exports) { var vals = this.sheet.getRange("A1:A"+this.sheet.getLastRow()).getValues(); for(var i = 0; i < this.sheet.getLastRow(); ++i) { if(vals[i][0] == key) { - this.sheet.getRange("C"+(i+1)).setValue(note); + this.sheet.getRange("D"+(i+1)).setValue(note); return; } } } - this.sheet.getRange("A"+(this.sheet.getLastRow()+1)+":C"+(this.sheet.getLastRow()+1)).setValues([[key, '', note]]); + this.sheet.getRange("A"+(this.sheet.getLastRow()+1)+":D"+(this.sheet.getLastRow()+1)).setValues([[key, '', note]]); return; }; diff --git a/scripts/gs_template.js b/scripts/gs_template.js index d318011..f7dc677 100644 --- a/scripts/gs_template.js +++ b/scripts/gs_template.js @@ -14,18 +14,21 @@ loadGSTemplate = function() { } else { var now = DateUtils.now(); - this.sheet.getRange("A1:L2").setValues([ + this.sheet.getRange("A1:N2").setValues([ [ - "出勤", "出勤更新", "退勤", "退勤更新", "休暇", "休暇取消", - "出勤中", "出勤なし", "休暇中", "休暇なし", "出勤確認", "退勤確認" + "出勤", "出勤更新", "退勤", "退勤更新", "休憩", "休暇", "休暇取消", + "出勤中", "出勤なし", "休暇中", "休暇なし", "出勤確認", "退勤確認", + "休憩エラー" ], [ "<@#1> Good morning (#2)!", "<@#1> I changed starting time to #2", "<@#1> Great work! (#2)", "<@#1> I changed leaving time to #2", + "<@#1> I changed break time to #2", "<@#1> I registered a holiday for #2", "<@#1> I canceled holiday #2", "#1 is working", "All staffs are working", "#2 is having a holiday at #1", "No one is having a holiday at #1", - "Is today holiday? #1", "Did you finish working today? #1" + "Is today holiday? #1", "Did you finish working today? #1", + "[Error] You have not started working today!" ] ]); } diff --git a/scripts/gs_timesheets.js b/scripts/gs_timesheets.js index d894f1c..de8fea0 100644 --- a/scripts/gs_timesheets.js +++ b/scripts/gs_timesheets.js @@ -12,6 +12,7 @@ loadGSTimesheets = function () { { name: '日付' }, { name: '出勤' }, { name: '退勤' }, + { name: '休憩(分)' }, { name: 'ノート' }, ], properties: [ @@ -69,17 +70,17 @@ loadGSTimesheets = function () { return v === '' ? undefined : v; }); - return({ user: username, date: row[0], signIn: row[1], signOut: row[2], note: row[3] }); + return({ user: username, date: row[0], signIn: row[1], signOut: row[2], break: row[3], note: row[4] }); }; GSTimesheets.prototype.set = function(username, date, params) { var row = this.get(username, date); - _.extend(row, _.pick(params, 'signIn', 'signOut', 'note')); + _.extend(row, _.pick(params, 'signIn', 'signOut', 'break', 'note')); var sheet = this._getSheet(username); var rowNo = this._getRowNo(username, date); - var data = [DateUtils.toDate(date), row.signIn, row.signOut, row.note].map(function(v) { + var data = [DateUtils.toDate(date), row.signIn, row.signOut, row.break, row.note].map(function(v) { return v == null ? '' : v; }); sheet.getRange("A"+rowNo+":"+String.fromCharCode(65 + this.scheme.columns.length - 1)+rowNo).setValues([data]); diff --git a/scripts/timesheets.js b/scripts/timesheets.js index e834cc4..66ad6a5 100644 --- a/scripts/timesheets.js +++ b/scripts/timesheets.js @@ -18,6 +18,7 @@ loadTimesheets = function (exports) { // 日付は先に処理しておく this.date = DateUtils.parseDate(message); this.time = DateUtils.parseTime(message); + this.minutes = DateUtils.parseMinutes(message) this.datetime = DateUtils.normalizeDateTime(this.date, this.time); if(this.datetime !== null) { this.dateStr = DateUtils.format("Y/m/d", this.datetime); @@ -29,6 +30,7 @@ loadTimesheets = function (exports) { ['actionSignOut', /(バ[ー〜ァ]*イ|ば[ー〜ぁ]*い|おやすみ|お[つっ]ー|おつ|さらば|お先|お疲|帰|乙|bye|night|(c|see)\s*(u|you)|left|退勤|ごきげんよ|グ[ッ]?バイ)/], ['actionWhoIsOff', /(だれ|誰|who\s*is).*(休|やす(ま|み|む))/], ['actionWhoIsIn', /(だれ|誰|who\s*is)/], + ['actionBreak', /(休憩|break)/], ['actionCancelOff', /(休|やす(ま|み|む)|休暇).*(キャンセル|消|止|やめ|ません)/], ['actionOff', /(休|やす(ま|み|む)|休暇)/], ['actionSignIn', /(モ[ー〜]+ニン|も[ー〜]+にん|おっは|おは|へろ|はろ|ヘロ|ハロ|hi|hello|morning|ohayo|出勤)/], @@ -83,6 +85,21 @@ loadTimesheets = function (exports) { } }; + // 休憩 + Timesheets.prototype.actionBreak = function(username, time) { + if (this.minutes) { + var data = this.storage.get(username, this.datetime); + if(!data.signIn || data.signIn === '-') { + // まだ出勤前である + this.responder.template("休憩エラー", username, "" ); + } else { + // break 入力 + this.storage.set(username, this.datetime, {break: this.minutes}); + this.responder.template("休憩", username, this.minutes + "分"); + } + } + }; + // 休暇申請 Timesheets.prototype.actionOff = function(username, message) { if(this.date) { diff --git a/testrunner.js b/testrunner.js index f67c1ae..0414a41 100644 --- a/testrunner.js +++ b/testrunner.js @@ -5,7 +5,9 @@ var runner = require("./node_modules/qunit"); runner.setup({ log: { assertions: true, - summary: true + summary: true, + coverage: true, + errors: true } }); @@ -34,4 +36,6 @@ runner.run([ "./scripts/date_utils.js", ] } -]); +], function(err, report) { + console.dir(report); +}); diff --git a/tests/date_utils_test.js b/tests/date_utils_test.js index 1787542..5eff7a9 100644 --- a/tests/date_utils_test.js +++ b/tests/date_utils_test.js @@ -12,6 +12,20 @@ QUnit.test( "DateUtils.parseTime", function(assert) { assert.ok(_.isEqual(null, DateUtils.parseTime("お昼")), "お昼"); }); +QUnit.test( "DateUtils.parseMinutes", function(assert) { + assert.ok(_.isEqual([90], DateUtils.parseMinutes("90分")), "90分"); + assert.ok(_.isEqual([90], DateUtils.parseMinutes("90 minutes")), "90 minutes"); + assert.ok(_.isEqual([90], DateUtils.parseMinutes("90 mins")), "90 mins"); + assert.ok(_.isEqual([1], DateUtils.parseMinutes("1 minute")), "1 minute"); + assert.ok(_.isEqual([90], DateUtils.parseMinutes("90 mins")), "90 mins"); + assert.ok(_.isEqual([90], DateUtils.parseMinutes("1.5時間")), "1.5時間"); + assert.ok(_.isEqual([60], DateUtils.parseMinutes("1時間")), "1時間"); + assert.ok(_.isEqual([60], DateUtils.parseMinutes("1 hour")), "1 hour"); + assert.ok(_.isEqual([60], DateUtils.parseMinutes("60分")), "60分"); + assert.ok(_.isEqual([120], DateUtils.parseMinutes("2 hours")), "2 hours"); + assert.ok(_.isEqual([135], DateUtils.parseMinutes("2時間15分")), "2時間15分"); +}); + QUnit.test( "DateUtils.parseDate", function(assert) { DateUtils.now(new Date(2016, 1-1, 1, 0, 0, 0)); assert.ok(_.isEqual([2015,12,1], DateUtils.parseDate("12/1")), "12/1"); diff --git a/tests/timesheets_test.js b/tests/timesheets_test.js index 2e97437..57db839 100644 --- a/tests/timesheets_test.js +++ b/tests/timesheets_test.js @@ -36,7 +36,7 @@ QUnit.test( "Timesheets", function(assert) { set: function(username, date, params) { var row = this.get(username, date); row.user = username; - _.extend(row, _.pick(params, 'signIn', 'signOut', 'note')); + _.extend(row, _.pick(params, 'signIn', 'signOut','break', 'note')); this.data[username][String(DateUtils.toDate(date))] = row; return row; }, @@ -143,6 +143,20 @@ QUnit.test( "Timesheets", function(assert) { msgTest('test1', 'お疲れさま 14:56', [['退勤更新', 'test1', "2014/01/02 14:56"]]); }); + // 休憩時間(出勤前) + var test1 = {}; + test1[nowDateStr()] = { user: 'test1', signIn: new Date(2014,0,2,0,0,0), signOut: new Date(2014,0,2,12,0,0) }; + storageTest({'test1': test1}, function(msgTest) { + msgTest('test1', '休憩 30分', [['休憩', 'test1', "30分"]]); + }); + + // 休憩時間(出勤後) + test1 = {}; + test1[nowDateStr()] = { user: 'test1', signIn: new Date(2014,0,2,0,0,0), signOut: new Date(2014,0,2,12,0,0) }; + storageTest({}, function(msgTest) { + msgTest('test1', '休憩 30分', [['休憩エラー', 'test1']]); + }); + // 休暇申請 storageTest({}, function(msgTest) { msgTest('test1', 'お休み', []);