WIP(teams): can do To Do's

This commit is contained in:
SabreCat
2022-01-28 17:14:33 -06:00
parent eaa5f821a4
commit 9e527f4f35
8 changed files with 125 additions and 73 deletions

View File

@@ -17,7 +17,6 @@ async function updateTeamTasks (team) {
) { ) {
const tasks = await Tasks.Task.find({ const tasks = await Tasks.Task.find({
'group.id': team._id, 'group.id': team._id,
'group.assignedUsers': [],
userId: { $exists: false }, userId: { $exists: false },
$or: [ $or: [
{ type: 'todo', completed: false }, { type: 'todo', completed: false },
@@ -51,9 +50,18 @@ async function updateTeamTasks (team) {
processChecklist = true; processChecklist = true;
daily.completed = false; daily.completed = false;
} else if (shouldDo(team.cron.lastProcessed, daily, teamLeader.preferences)) { } else if (shouldDo(team.cron.lastProcessed, daily, teamLeader.preferences)) {
let assignments = 0;
let completions = 0;
for (const assignedUser in daily.group.assignedUsers) {
if (Object.prototype.hasOwnProperty.call(daily.group.assignedUsers, assignedUser)) {
assignments += 1;
if (assignedUser.completed) completions += 1;
assignedUser.completed = false;
}
}
processChecklist = true; processChecklist = true;
const delta = TASK_VALUE_CHANGE_FACTOR ** daily.value; const delta = TASK_VALUE_CHANGE_FACTOR ** daily.value;
daily.value -= delta; daily.value -= ((completions / assignments) * delta);
if (daily.value < MIN_TASK_VALUE) daily.value = MIN_TASK_VALUE; if (daily.value < MIN_TASK_VALUE) daily.value = MIN_TASK_VALUE;
} }
daily.isDue = shouldDo(new Date(), daily, teamLeader.preferences); daily.isDue = shouldDo(new Date(), daily, teamLeader.preferences);

View File

@@ -58,8 +58,8 @@
class="task-control daily-todo-control" class="task-control daily-todo-control"
:class="controlClass.inner" :class="controlClass.inner"
tabindex="0" tabindex="0"
@click="score(task.completed ? 'down' : 'up' )" @click="score(showCheckIcon ? 'down' : 'up' )"
@keypress.enter="score(task.completed ? 'down' : 'up' )" @keypress.enter="score(showCheckIcon ? 'down' : 'up' )"
> >
<div <div
v-if="showTaskLockIcon" v-if="showTaskLockIcon"
@@ -71,7 +71,7 @@
v-else v-else
class="svg-icon check" class="svg-icon check"
:class="{ :class="{
'display-check-icon': task.completed, 'display-check-icon': showCheckIcon,
[controlClass.checkbox]: true, [controlClass.checkbox]: true,
}" }"
v-html="icons.check" v-html="icons.check"
@@ -1043,12 +1043,22 @@ export default {
if (this.task.group.assignedUsers) return false; if (this.task.group.assignedUsers) return false;
return true; return true;
}, },
showCheckIcon () {
if (this.isGroupTask && this.task.group.assignedUsers
&& this.task.group.assignedUsers[this.user._id]) {
return this.task.group.assignedUsers[this.user._id].completed;
}
return this.task.completed;
},
showTaskLockIcon () { showTaskLockIcon () {
if (this.isUser) return false; if (this.isUser) return false;
if (this.isGroupTask) { if (this.isGroupTask) {
if (this.task.completed) { if (this.task.completed) {
if (this.task.group.completedBy === this.user._id) return false; if (this.task.group.assignedUsers && this.task.group.assignedUsers[this.user._id]) {
if (this.teamManagerAccess) return false; return false;
}
if (this.task.group.completedBy.userId === this.user._id) return false;
if (this.teamManagerAccess && !this.task.group.assignedUsers) return false;
return true; return true;
} }
if (this.isOpenTask) return false; if (this.isOpenTask) return false;

View File

@@ -176,8 +176,8 @@
"onlyGroupTasksCanBeAssigned": "Only group tasks can be assigned", "onlyGroupTasksCanBeAssigned": "Only group tasks can be assigned",
"assignedTo": "Assign To", "assignedTo": "Assign To",
"assignedToUser": "Assigned to <strong><%- userName %></strong>", "assignedToUser": "Assigned to <strong><%- userName %></strong>",
"assignedToMembers": "Assigned to <strong><%= userCount %> members</strong>", "assignedToMembers": "Assigned to <strong><%= userCount %> users</strong>",
"assignedToYouAndMembers": "Assigned to you and <strong><%= userCount %> members</strong>", "assignedToYouAndMembers": "Assigned to you and <strong><%= userCount %> users</strong>",
"youAreAssigned": "Assigned to you", "youAreAssigned": "Assigned to you",
"taskIsUnassigned": "This task is unassigned", "taskIsUnassigned": "This task is unassigned",
"unassigned": "Unassigned", "unassigned": "Unassigned",

View File

@@ -1,3 +1,4 @@
import find from 'lodash/find';
import timesLodash from 'lodash/times'; import timesLodash from 'lodash/times';
import reduce from 'lodash/reduce'; import reduce from 'lodash/reduce';
import moment from 'moment'; import moment from 'moment';
@@ -330,13 +331,37 @@ export default function scoreTask (options = {}, req = {}, analytics) {
delta += _changeTaskValue(user, task, direction, times, cron); delta += _changeTaskValue(user, task, direction, times, cron);
} else { } else {
if (direction === 'up') { if (direction === 'up') {
if (task.group.id) {
if (!task.group.assignedUsers) {
task.group.completedBy = {
userId: user._id,
date: new Date(),
};
task.completed = true;
} else {
task.group.assignedUsers[user._id].completed = true;
task.group.assignedUsers[user._id].completedDate = new Date();
if (!find(task.group.assignedUsers, assignedUser => !assignedUser.completed)) {
task.dateCompleted = new Date(); task.dateCompleted = new Date();
task.completed = true; task.completed = true;
if (task.group) task.group.completedBy = user._id; }
}
if (task.markModified) task.markModified('group');
} else {
task.dateCompleted = new Date();
task.completed = true;
}
} else if (direction === 'down') { } else if (direction === 'down') {
task.completed = false; task.completed = false;
task.dateCompleted = undefined; task.dateCompleted = undefined;
if (task.group && task.group.completedBy) task.group.completedBy = undefined; if (task.group.id) {
if (task.group.completedBy) task.group.completedBy = {};
if (task.group.assignedUsers && task.group.assignedUsers[user._id]) {
task.group.assignedUsers[user._id].completed = false;
task.group.assignedUsers[user._id].completedDate = undefined;
}
if (task.markModified) task.markModified('group');
}
} }
delta += _changeTaskValue(user, task, direction, times, cron); delta += _changeTaskValue(user, task, direction, times, cron);

View File

@@ -688,7 +688,7 @@ api.updateTask = {
setNextDue(task, user); setNextDue(task, user);
const savedTask = await task.save(); const savedTask = await task.save();
if (group && task.group.id && task.group.assignedUsers.length > 0) { if (group && task.group.id && task.group.assignedUsers) {
const updateCheckListItems = _.remove(sanitizedObj.checklist, checklist => { const updateCheckListItems = _.remove(sanitizedObj.checklist, checklist => {
const indexOld = _.findIndex(oldCheckList, check => check.id === checklist.id); const indexOld = _.findIndex(oldCheckList, check => check.id === checklist.id);
if (indexOld !== -1) return checklist.text !== oldCheckList[indexOld].text; if (indexOld !== -1) return checklist.text !== oldCheckList[indexOld].text;
@@ -702,7 +702,7 @@ api.updateTask = {
if (challenge) { if (challenge) {
challenge.updateTask(savedTask); challenge.updateTask(savedTask);
} else if (group && task.group.id && task.group.assignedUsers.length > 0) { } else if (group && task.group.id && task.group.assignedUsers) {
await group.updateTask(savedTask); await group.updateTask(savedTask);
} else { } else {
taskActivityWebhook.send(user, { taskActivityWebhook.send(user, {

View File

@@ -305,7 +305,7 @@ async function handleTeamTask (task, delta, direction) {
if (teamTask) { if (teamTask) {
const groupDelta = teamTask.group.assignedUsers const groupDelta = teamTask.group.assignedUsers
? delta / teamTask.group.assignedUsers.length ? delta / _.keys(teamTask.group.assignedUsers).length
: delta; : delta;
await teamTask.scoreChallengeTask(groupDelta, direction); await teamTask.scoreChallengeTask(groupDelta, direction);
if (task.type === 'daily' || task.type === 'todo') { if (task.type === 'daily' || task.type === 'todo') {
@@ -328,14 +328,19 @@ async function handleTeamTask (task, delta, direction) {
*/ */
async function scoreTask (user, task, direction, req, res) { async function scoreTask (user, task, direction, req, res) {
if (task.type === 'daily' || task.type === 'todo') { if (task.type === 'daily' || task.type === 'todo') {
if (task.completed && direction === 'up') { if (task.group.id && task.group.assignedUsers) {
if (task.group.assignedUsers[user._id].completed && direction === 'up') {
throw new NotAuthorized(res.t('sessionOutdated'));
} else if (!task.group.assignedUsers[user._id].completed && direction === 'down') {
throw new NotAuthorized(res.t('sessionOutdated'));
}
} else if (task.completed && direction === 'up') {
throw new NotAuthorized(res.t('sessionOutdated')); throw new NotAuthorized(res.t('sessionOutdated'));
} else if (!task.completed && direction === 'down') { } else if (!task.completed && direction === 'down') {
throw new NotAuthorized(res.t('sessionOutdated')); throw new NotAuthorized(res.t('sessionOutdated'));
} }
} }
let localTask;
let rollbackUser; let rollbackUser;
let group; let group;
@@ -347,46 +352,36 @@ async function scoreTask (user, task, direction, req, res) {
}); });
} }
if ( if (
group && task.group.id && !task.userId group && task.group.id && !task.userId // Task is on team board
&& direction === 'down' && ['todo', 'daily'].includes(task.type) // Task is a To Do or Daily
&& ['todo', 'daily'].includes(task.type) && direction === 'down' // Task is being "unchecked"
&& task.completed ) {
&& task.group.completedBy !== user._id const userIsManagement = group.leader === user._id || Boolean(group.managers[user._id]);
if (!userIsManagement
&& !(task.group.completedBy && task.group.completedBy.userId === user._id)
&& !(task.group.assignedUsers && task.group.assignedUsers[user._id])
) { ) {
if (group.leader !== user._id && !group.managers[user._id]) {
throw new BadRequest('Cannot uncheck task you did not complete if not a manager.'); throw new BadRequest('Cannot uncheck task you did not complete if not a manager.');
} }
rollbackUser = await User.findOne({ _id: task.group.completedBy }); rollbackUser = await User.findOne({ _id: task.group.completedBy.userId });
task.group.completedBy = undefined; task.group.completedBy = {};
} else if (task.group.id && !task.userId && task.group.assignedUsers.length > 0) {
// Task is being scored from team board, and a user copy should exist
if (!task.group.assignedUsers.includes(user._id)) {
throw new BadRequest('Task has not been assigned to this user.');
} }
localTask = await Tasks.Task.findOne( const wasCompleted = task.completed;
{ userId: user._id, 'group.taskId': task._id },
).exec();
if (!localTask) throw new NotFound('Task not found.');
}
const targetTask = localTask || task;
const wasCompleted = targetTask.completed;
const firstTask = !user.achievements.completedTask; const firstTask = !user.achievements.completedTask;
let delta; let delta;
if (rollbackUser) { if (rollbackUser) {
delta = shared.ops.scoreTask({ delta = shared.ops.scoreTask({
task: targetTask, task,
user: rollbackUser, user: rollbackUser,
direction, direction,
}, req, res.analytics); }, req, res.analytics);
rollbackUser.addNotification('GROUP_TASK_NEEDS_WORK', { rollbackUser.addNotification('GROUP_TASK_NEEDS_WORK', {
message: res.t('taskNeedsWork', { taskText: targetTask.text, managerName: user.profile.name }, rollbackUser.preferences.language), message: res.t('taskNeedsWork', { taskText: task.text, managerName: user.profile.name }, rollbackUser.preferences.language),
task: { task: {
id: targetTask._id, id: task._id,
text: targetTask.text, text: task.text,
}, },
group: { group: {
id: group._id, id: group._id,
@@ -399,51 +394,70 @@ async function scoreTask (user, task, direction, req, res) {
}); });
await rollbackUser.save(); await rollbackUser.save();
} else { } else {
delta = shared.ops.scoreTask({ task: targetTask, user, direction }, req, res.analytics); delta = shared.ops.scoreTask({ task, user, direction }, req, res.analytics);
} }
// Drop system (don't run on the client, // Drop system (don't run on the client,
// as it would only be discarded since ops are sent to the API, not the results) // 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: targetTask, delta }, req, res.analytics); if (direction === 'up' && !firstTask) shared.fns.randomDrop(user, { task, delta }, req, res.analytics);
// If a todo was completed or uncompleted move it in or out of the user.tasksOrder.todos list // If a todo was completed or uncompleted move it in or out of the user.tasksOrder.todos list
// TODO move to common code? // TODO move to common code?
let pullTask; let pullTask;
let pushTask; let pushTask;
if (targetTask.type === 'todo') { if (task.type === 'todo') {
if (!wasCompleted && task.completed) { if (!wasCompleted && task.completed) {
// @TODO: mongoose's push and pull should be atomic and help with // @TODO: mongoose's push and pull should be atomic and help with
// our concurrency issues. If not, we need to use this update $pull and $push // our concurrency issues. If not, we need to use this update $pull and $push
pullTask = targetTask._id; pullTask = task._id;
} else if ( } else if (
wasCompleted wasCompleted
&& !targetTask.completed && !task.completed
&& user.tasksOrder.todos.indexOf(task._id) === -1 && user.tasksOrder.todos.indexOf(task._id) === -1
) { ) {
pushTask = targetTask._id; pushTask = task._id;
} }
} }
if (targetTask.completed && targetTask.group.id && !targetTask.userId) { if (task.completed && task.group.id
targetTask.group.completedBy = user._id; && !task.userId && !task.group.assignedUsers) {
task.group.completedBy = {
userId: user._id,
date: new Date(),
};
} }
setNextDue(task, user); setNextDue(task, user);
if (localTask) {
localTask.completed = targetTask.completed;
localTask.value = Number(targetTask.value) + Number(delta);
await localTask.save();
}
taskScoredWebhook.send(user, { taskScoredWebhook.send(user, {
task: targetTask, task,
direction, direction,
delta, delta,
user, user,
}); });
if (group) {
let role;
if (group.leader === user._id) {
role = 'leader';
} else if (group.managers[user._id]) {
role = 'manager';
} else {
role = 'member';
}
res.analytics.track('team task scored', {
uuid: user._id,
hitType: 'event',
category: 'behavior',
taskType: task.type,
direction,
headers: req.headers,
groupID: group._id,
role,
});
}
return { return {
task: targetTask, task,
delta, delta,
direction, direction,
pullTask, pullTask,

View File

@@ -1466,9 +1466,7 @@ schema.methods.updateTask = async function updateTask (taskToSync, options = {})
updateCmd.$set[key] = syncableAttributes[key]; updateCmd.$set[key] = syncableAttributes[key];
} }
updateCmd.$set['group.approval.required'] = taskToSync.group.approval.required;
updateCmd.$set['group.assignedUsers'] = taskToSync.group.assignedUsers; updateCmd.$set['group.assignedUsers'] = taskToSync.group.assignedUsers;
updateCmd.$set['group.sharedCompletion'] = taskToSync.group.sharedCompletion;
updateCmd.$set['group.managerNotes'] = taskToSync.group.managerNotes; updateCmd.$set['group.managerNotes'] = taskToSync.group.managerNotes;
const taskSchema = Tasks[taskToSync.type]; const taskSchema = Tasks[taskToSync.type];
@@ -1516,6 +1514,7 @@ schema.methods.syncTask = async function groupSyncTask (taskToSync, users, assig
if (!taskToSync.group.assignedUsers[user._id]) { if (!taskToSync.group.assignedUsers[user._id]) {
taskToSync.group.assignedUsers[user._id] = assignmentData; taskToSync.group.assignedUsers[user._id] = assignmentData;
} }
taskToSync.markModified('group.assignedUsers');
// Sync tags // Sync tags
const userTags = user.tags; const userTags = user.tags;
@@ -1556,7 +1555,6 @@ schema.methods.syncTask = async function groupSyncTask (taskToSync, users, assig
if (orderList.indexOf(matchingTask._id) === -1 && (matchingTask.type !== 'todo' || !matchingTask.completed)) orderList.push(matchingTask._id); if (orderList.indexOf(matchingTask._id) === -1 && (matchingTask.type !== 'todo' || !matchingTask.completed)) orderList.push(matchingTask._id);
} }
matchingTask.group.assignedUsers = taskToSync.group.assignedUsers; matchingTask.group.assignedUsers = taskToSync.group.assignedUsers;
matchingTask.group.sharedCompletion = taskToSync.group.sharedCompletion;
matchingTask.group.managerNotes = taskToSync.group.managerNotes; matchingTask.group.managerNotes = taskToSync.group.managerNotes;
// sync checklist // sync checklist
@@ -1589,8 +1587,9 @@ schema.methods.unlinkTask = async function groupUnlinkTask (
userId: user._id, userId: user._id,
}; };
const assignedUserIndex = unlinkingTask.group.assignedUsers.indexOf(user._id); delete unlinkingTask.group.assignedUsers[user._id];
unlinkingTask.group.assignedUsers.splice(assignedUserIndex, 1); unlinkingTask.markModified('group.assignedUsers');
const promises = [unlinkingTask.save()];
if (keep === 'keep-all') { if (keep === 'keep-all') {
await Tasks.Task.update(findQuery, { await Tasks.Task.update(findQuery, {
@@ -1608,16 +1607,14 @@ schema.methods.unlinkTask = async function groupUnlinkTask (
user.markModified('tasksOrder'); user.markModified('tasksOrder');
} }
const promises = [unlinkingTask.save()];
if (task) { if (task) {
promises.push(task.remove()); promises.push(task.remove());
} }
// When multiple tasks are being unlinked at the same time, // When multiple tasks are being unlinked at the same time,
// save the user once outside of this function // save the user once outside of this function
if (saveUser) promises.push(user.save()); if (saveUser) promises.push(user.save());
await Promise.all(promises);
} }
await Promise.all(promises);
}; };
schema.methods.removeTask = async function groupRemoveTask (task) { schema.methods.removeTask = async function groupRemoveTask (task) {

View File

@@ -133,16 +133,14 @@ export const TaskSchema = new Schema({
// key is assigned UUID, with // key is assigned UUID, with
// { assignedDate: Date, // { assignedDate: Date,
// assigningUsername: '@username', // assigningUsername: '@username',
// completed: Boolean } // completed: Boolean,
// completedDate: Date }
}, },
taskId: { $type: String, ref: 'Task', validate: [v => validator.isUUID(v), 'Invalid uuid for group task.'] }, taskId: { $type: String, ref: 'Task', validate: [v => validator.isUUID(v), 'Invalid uuid for group task.'] },
sharedCompletion: {
$type: String, default: 'singleCompletion', // legacy data
},
managerNotes: { $type: String }, managerNotes: { $type: String },
completedBy: { completedBy: {
$type: Schema.Types.Mixed, userId: { $type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid for task completing user.'] },
default: () => ({}), // { 'UUID': Date } date: { $type: Date },
}, },
}, },