diff --git a/website/client/src/components/group-plans/taskInformation.vue b/website/client/src/components/group-plans/taskInformation.vue index ccc79a9198..db197c1d8b 100644 --- a/website/client/src/components/group-plans/taskInformation.vue +++ b/website/client/src/components/group-plans/taskInformation.vue @@ -222,6 +222,7 @@ export default { this.$root.$on('habitica:team-sync', () => { this.loadTasks(); + this.loadGroupCompletedTodos(); }); }, methods: { diff --git a/website/client/src/components/tasks/task.vue b/website/client/src/components/tasks/task.vue index becdd85e9f..1b59fb4600 100644 --- a/website/client/src/components/tasks/task.vue +++ b/website/client/src/components/tasks/task.vue @@ -1046,9 +1046,13 @@ export default { showTaskLockIcon () { if (this.isUser) return false; if (this.isGroupTask) { - if (this.isOpenTask) return false; + if (this.teamManagerAccess) return false; + if (this.isOpenTask) { + if (!this.task.completed) return false; + if (this.task.group.completedBy === this.user._id) return false; + return true; + } if (this.task.group.assignedUsers.indexOf(this.user._id) !== -1) return false; - if (this.teamManagerAccess && this.task.completed) return false; } return true; }, diff --git a/website/client/src/mixins/scoreTask.js b/website/client/src/mixins/scoreTask.js index 91cf72ea4d..c1622cb395 100644 --- a/website/client/src/mixins/scoreTask.js +++ b/website/client/src/mixins/scoreTask.js @@ -56,11 +56,15 @@ export default { const canScoreTask = await this.beforeTaskScore(task); if (!canScoreTask) return; - try { - scoreTask({ task, user, direction }); - } catch (err) { - this.text(err.message); - return; + if (direction === 'down' && task.group.completedBy && task.group.completedBy !== user._id) { + task.completed = false; + } else { + try { + scoreTask({ task, user, direction }); + } catch (err) { + this.text(err.message); + return; + } } this.playTaskScoreSound(task, direction); diff --git a/website/server/libs/tasks/index.js b/website/server/libs/tasks/index.js index 793f4087a1..8bff8f98f7 100644 --- a/website/server/libs/tasks/index.js +++ b/website/server/libs/tasks/index.js @@ -8,6 +8,7 @@ import { } from './utils'; import { model as Challenge } from '../../models/challenge'; import { model as Group } from '../../models/group'; +import { model as User } from '../../models/user'; import * as Tasks from '../../models/task'; import apiError from '../apiError'; import { @@ -350,9 +351,34 @@ async function scoreTask (user, task, direction, req, res) { } const wasCompleted = task.completed; - const firstTask = !user.achievements.completedTask; - const [delta] = shared.ops.scoreTask({ task, user, direction }, req, res.analytics); + let delta; + let rollbackUser; + + if ( + task.group.id && !task.userId + && direction === 'down' + && ['todo', 'daily'].includes(task.type) + && task.completed + && task.group.completedBy !== user._id + ) { + const group = await Group.getGroup({ + user, + groupId: task.group.id, + fields: 'leader managers', + }); + if (group.leader !== user._id && !group.managers[user._id]) { + throw new BadRequest('Cannot uncheck task you did not complete if not a manager.'); + } + rollbackUser = await User.findOne({ _id: task.group.completedBy }); + } + + if (rollbackUser) { + delta = shared.ops.scoreTask({ task, user: rollbackUser, direction }, req, res.analytics); + await rollbackUser.save(); + } else { + delta = shared.ops.scoreTask({ task, user, direction }, req, res.analytics); + } // Drop system (don't run on the client, // as it would only be discarded since ops are sent to the API, not the results) if (direction === 'up' && !firstTask) shared.fns.randomDrop(user, { task, delta }, req, res.analytics);