diff --git a/common/script/index.coffee b/common/script/index.coffee index f251ad7db5..5c44457319 100644 --- a/common/script/index.coffee +++ b/common/script/index.coffee @@ -72,13 +72,51 @@ api.daysSince = (yesterday, options = {}) -> Math.abs api.startOfDay(_.defaults {now:yesterday}, o).diff(api.startOfDay(_.defaults {now:o.now}, o), 'days') ### - Should the user do this taks on this date, given the task's repeat options and user.preferences.dayStart? + Should the user do this task on this date, given the task's repeat options and user.preferences.dayStart? ### -api.shouldDo = (day, repeat, options={}) -> - return false unless repeat +api.shouldDo = (day, dailyTask, options = {}) -> + return false unless dailyTask.type == 'daily' && dailyTask.repeat + # MIGRATION HACK, FIXME + if dailyTask.startDate == null + console.log("null!!!!!") + dailyTask.startDate = moment().toDate(); + if dailyTask.startDate instanceof String + console.log("startDate is a string: " + dailyTask.startDate + ". Converting to date"); + dailyTask.startDate = moment(dailyTask.startDate).toDate(); o = sanitizeOptions options - selected = repeat[api.dayMapping[api.startOfDay(_.defaults {now:day}, o).day()]] - return selected + dayOfWeek = api.startOfDay(_.defaults {now:day}, o).day() + + # check if event is in the future + hasStartedCheck = moment(day).isAfter(dailyTask.startDate) || moment(day).isSame(dailyTask.startDate) + + if dailyTask.frequency == 'daily' + daysSinceTaskStart = api.numDaysApart(day, dailyTask.startDate, o) + everyXCheck = (daysSinceTaskStart % dailyTask.everyX == 0) + return everyXCheck && hasStartedCheck + else if dailyTask.frequency == 'weekly' + dayOfWeekCheck = dailyTask.repeat[api.dayMapping[dayOfWeek]]; + weeksSinceTaskStartWeek = api.numWeeksApart(day, dailyTask.startDate, o) + everyXCheck = (weeksSinceTaskStartWeek % dailyTask.everyX == 0) + return dayOfWeekCheck && everyXCheck && hasStartedCheck + else + # unexpected frequency string + return true + +api.numDaysApart = (day1, day2, o) -> + startOfDay1 = api.startOfDay(_.defaults {now:day1}, o) + startOfDay2 = api.startOfDay(_.defaults {now:day2}, o) + numDays = Math.abs(startOfDay1.diff(startOfDay2, 'days')) + return numDays + +# weeks between the two days, counting Monday as the start of each week +api.numWeeksApart = (day1, day2, o) -> + startOfDay1 = api.startOfDay(_.defaults {now:day1}, o) + startOfDay2 = api.startOfDay(_.defaults {now:day2}, o) + startWeekOfDay1 = startOfDay1.startOf('week') + startWeekOfDay2 = startOfDay2.startOf('week') + numWeeks = Math.abs(startWeekOfDay1.diff(startWeekOfDay2, 'weeks')) + return numWeeks + ### ------------------------------------------------------ @@ -280,7 +318,7 @@ api.taskClasses = (task, filters=[], dayStart=0, lastCron=+new Date, showComplet # show as completed if completed (naturally) or not required for today if type in ['todo', 'daily'] - if completed or (type is 'daily' and !api.shouldDo(+new Date, task.repeat, {dayStart})) + if completed or (type is 'daily' and !api.shouldDo(+new Date, task, {dayStart})) classes += " completed" else classes += " uncompleted" @@ -1537,7 +1575,7 @@ api.wrap = (user, main=true) -> scheduleMisses = 0 _.times daysMissed, (n) -> thatDay = moment(now).subtract({days: n + 1}) - if api.shouldDo(thatDay, repeat, user.preferences) + if api.shouldDo(thatDay, task, user.preferences) scheduleMisses++ if user.stats.buffs.stealth user.stats.buffs.stealth-- diff --git a/website/public/js/controllers/tasksCtrl.js b/website/public/js/controllers/tasksCtrl.js index c070727f8f..41ec17655a 100644 --- a/website/public/js/controllers/tasksCtrl.js +++ b/website/public/js/controllers/tasksCtrl.js @@ -4,6 +4,9 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N function($scope, $rootScope, $location, User, Notification, $http, ApiUrl, $timeout, Shared, Guide) { $scope.obj = User.user; // used for task-lists $scope.user = User.user; + // HACK: flagDict is for storing whether the datePicker popup is open or not. This should just be a boolean flag, + // but apparently due to angular bug need to put the bool in intermediate dict... + $scope.flagDict = {}; $scope.score = function(task, direction) { switch (task.type) { @@ -127,6 +130,24 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N */ $scope._today = moment().add({days: 1}); + /* + ------------------------ + Dailies + ------------------------ + */ + $scope.updateTaskStartDate = function(task) { + // TODO: Weirdness here...startDate should be a Date and _dateString + // should be a string, but the datePicker input fields sets _dateString + // to a Date even though input type="text"... + task.startDate = task._dateString; + }; + + $scope.open = function($event) { + $event.stopPropagation(); + $scope.flagDict['opened'] = !$scope.flagDict['opened']; + } + + /* ------------------------ Checklists @@ -212,7 +233,7 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N $scope.shouldShow = function(task, list, prefs){ if (task._editing) // never hide a task while being edited return true; - var shouldDo = task.type == 'daily' ? habitrpgShared.shouldDo(new Date, task.repeat, prefs) : true; + var shouldDo = task.type == 'daily' ? habitrpgShared.shouldDo(new Date, task, prefs) : true; switch (list.view) { case "yellowred": // Habits return task.value < 1; diff --git a/website/public/js/directives/directives.js b/website/public/js/directives/directives.js index 424913d00e..92a596af70 100644 --- a/website/public/js/directives/directives.js +++ b/website/public/js/directives/directives.js @@ -45,6 +45,7 @@ habitrpg task._editing = !task._editing; task._tags = User.user.preferences.tagsCollapsed; task._advanced = User.user.preferences.advancedCollapsed; + task._dateString = moment(task.startDate).format('YYYY-MM-DD'); if($rootScope.charts[task.id]) $rootScope.charts[task.id] = false; }; }], diff --git a/website/src/models/task.js b/website/src/models/task.js index 2646263e80..14ee018439 100644 --- a/website/src/models/task.js +++ b/website/src/models/task.js @@ -50,10 +50,14 @@ var checklist = [{ var DailySchema = new Schema( _.defaults({ - type: {type:String, 'default': 'daily'}, + type: {type: String, 'default': 'daily'}, + //TODO: Cleaner to store interval as enum instead of str? + frequency: {type: String, 'default': 'daily'}, // 'daily', 'weekly' + everyX: {type: Number, 'default': 1}, // e.g. once every X weeks + startDate: {type: Date}, history: Array, completed: {type: Boolean, 'default': false}, - repeat: { + repeat: { // used only for 'weekly' frequency, TODO: Rename to daysOfWeek or something with 'week' m: {type: Boolean, 'default': true}, t: {type: Boolean, 'default': true}, w: {type: Boolean, 'default': true}, diff --git a/website/views/shared/tasks/task.jade b/website/views/shared/tasks/task.jade index 05661a939e..2afe1608a0 100644 --- a/website/views/shared/tasks/task.jade +++ b/website/views/shared/tasks/task.jade @@ -166,22 +166,56 @@ li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s // if Daily, calendar fieldset(ng-if='::task.type=="daily"', class="option-group") legend.option-title=env.t('repeat') - ul.repeat-days(bindonce) - // note, does not use data-toggle="buttons-checkbox" - it would interfere with our own click binding + + // Choose the frequency + ul.priority-multiplier li - button(ng-class='{active: task.repeat.su}', type='button', ng-click='task.challenge.id || (task.repeat.su = !task.repeat.su)') {{::moment.weekdaysMin(0)}} + button(ng-class='{active: task.frequency=="daily"}', type='button', ng-click='task.frequency="daily"')='Daily' li - button(ng-class='{active: task.repeat.m}', type='button', ng-click='task.challenge.id || (task.repeat.m = !task.repeat.m)') {{::moment.weekdaysMin(1)}} - li - button(ng-class='{active: task.repeat.t}', type='button', ng-click='task.challenge.id || (task.repeat.t = !task.repeat.t)') {{::moment.weekdaysMin(2)}} - li - button(ng-class='{active: task.repeat.w}', type='button', ng-click='task.challenge.id || (task.repeat.w = !task.repeat.w)') {{::moment.weekdaysMin(3)}} - li - button(ng-class='{active: task.repeat.th}', type='button', ng-click='task.challenge.id || (task.repeat.th = !task.repeat.th)') {{::moment.weekdaysMin(4)}} - li - button(ng-class='{active: task.repeat.f}', type='button', ng-click='task.challenge.id || (task.repeat.f= !task.repeat.f)') {{::moment.weekdaysMin(5)}} - li - button(ng-class='{active: task.repeat.s}', type='button', ng-click='task.challenge.id || (task.repeat.s = !task.repeat.s)') {{::moment.weekdaysMin(6)}} + button(ng-class='{active: task.frequency=="weekly"}', type='button', ng-click='task.frequency="weekly"')='Weekly' + + // If frequency is daily + fieldset(ng-show='task.frequency=="daily"') + | Once every   + select(ng-model='task.everyX') + option(ng-repeat='x in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]') {{x}} + | days

+ | Start date:  + p(class='input-group') + input(type='text' class='form-control' datepicker-popup ng-model='task._dateString' ng-change='updateTaskStartDate(task)' is-open='flagDict.opened') + span(class='input-group-btn') + button(type="button" ng-click="open($event)") + span.input-group-addon.glyphicon.glyphicon-calendar + // If frequency is weekly + fieldset(ng-show='task.frequency=="weekly"') + | Once every  + select(ng-model='task.everyX') + option(ng-repeat='x in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]') {{x}} + | weeks on these days:

+ ul.repeat-days(bindonce) + // note, does not use data-toggle="buttons-checkbox" - it would interfere with our own click binding + li + button(ng-class='{active: task.repeat.su}', type='button', ng-click='task.challenge.id || (task.repeat.su = !task.repeat.su)') {{::moment.weekdaysMin(0)}} + li + button(ng-class='{active: task.repeat.m}', type='button', ng-click='task.challenge.id || (task.repeat.m = !task.repeat.m)') {{::moment.weekdaysMin(1)}} + li + button(ng-class='{active: task.repeat.t}', type='button', ng-click='task.challenge.id || (task.repeat.t = !task.repeat.t)') {{::moment.weekdaysMin(2)}} + li + button(ng-class='{active: task.repeat.w}', type='button', ng-click='task.challenge.id || (task.repeat.w = !task.repeat.w)') {{::moment.weekdaysMin(3)}} + li + button(ng-class='{active: task.repeat.th}', type='button', ng-click='task.challenge.id || (task.repeat.th = !task.repeat.th)') {{::moment.weekdaysMin(4)}} + li + button(ng-class='{active: task.repeat.f}', type='button', ng-click='task.challenge.id || (task.repeat.f= !task.repeat.f)') {{::moment.weekdaysMin(5)}} + li + button(ng-class='{active: task.repeat.s}', type='button', ng-click='task.challenge.id || (task.repeat.s = !task.repeat.s)') {{::moment.weekdaysMin(6)}} + |

+ | Start date:  + p(class='input-group') + input(type='text' class='form-control' datepicker-popup ng-model='task._dateString' ng-change='updateTaskStartDate(task)' is-open='flagDict.opened') + span(class='input-group-btn') + button(type="button" ng-click="open($event)") + span.input-group-addon.glyphicon.glyphicon-calendar + // if Reward, pricing fieldset.option-group.option-short(ng-if='task.type=="reward" && !task.challenge.id')