diff --git a/packages/central-server/src/apiV2/tasks/CreateTask.js b/packages/central-server/src/apiV2/tasks/CreateTask.js index 0d648537c6..dc79ec9d4d 100644 --- a/packages/central-server/src/apiV2/tasks/CreateTask.js +++ b/packages/central-server/src/apiV2/tasks/CreateTask.js @@ -21,13 +21,19 @@ export class CreateTask extends CreateHandler { } async createRecord() { - const { comment, ...data } = this.newRecordData; - - return this.models.wrapInTransaction(async transactingModels => { - const task = await transactingModels.task.create(data); + const { comment, ...newRecordData } = this.newRecordData; + await this.models.wrapInTransaction(async transactingModels => { + const task = await transactingModels.task.create(newRecordData); + // Add the user comment first, since the transaction will mean that all comments have the same created_at time, but we want the user comment to be the 'most recent' if (comment) { - await task.addComment(comment, this.req.user.id); + await task.addComment(comment, this.req.user.id, transactingModels.taskComment.types.User); } + await task.addComment( + 'Created this task', + this.req.user.id, + transactingModels.taskComment.types.System, + ); + return { id: task.id, }; diff --git a/packages/central-server/src/apiV2/tasks/EditTask.js b/packages/central-server/src/apiV2/tasks/EditTask.js index 0f06ee1ea7..db20be926c 100644 --- a/packages/central-server/src/apiV2/tasks/EditTask.js +++ b/packages/central-server/src/apiV2/tasks/EditTask.js @@ -26,6 +26,7 @@ export class EditTask extends EditHandler { if (comment) { await originalTask.addComment(comment, this.req.user.id); } + await originalTask.addSystemCommentsOnUpdate(this.updatedFields, this.req.user.id); return task; }); } diff --git a/packages/central-server/src/tests/apiV2/tasks/EditTask.test.js b/packages/central-server/src/tests/apiV2/tasks/EditTask.test.js index a0f51382b0..8f1855e7cd 100644 --- a/packages/central-server/src/tests/apiV2/tasks/EditTask.test.js +++ b/packages/central-server/src/tests/apiV2/tasks/EditTask.test.js @@ -129,83 +129,267 @@ describe('Permissions checker for EditTask', async () => { }); describe('PUT /tasks/:id', async () => { - it('Sufficient permissions: allows a user to edit a task if they have BES Admin permission', async () => { - await app.grantAccess(BES_ADMIN_POLICY); - await app.put(`tasks/${tasks[1].id}`, { - body: { - entity_id: facilities[0].id, - survey_id: surveys[0].survey.id, - }, + describe('Permissions', async () => { + it('Sufficient permissions: allows a user to edit a task if they have BES Admin permission', async () => { + await app.grantAccess(BES_ADMIN_POLICY); + await app.put(`tasks/${tasks[1].id}`, { + body: { + entity_id: facilities[0].id, + survey_id: surveys[0].survey.id, + }, + }); + const result = await models.task.find({ + id: tasks[1].id, + }); + expect(result[0].entity_id).to.equal(facilities[0].id); + expect(result[0].survey_id).to.equal(surveys[0].survey.id); }); - const result = await models.task.find({ - id: tasks[1].id, + + it('Sufficient permissions: allows a user to edit a task if they have access to the task, and entity and survey are not being updated', async () => { + await app.grantAccess(DEFAULT_POLICY); + await app.put(`tasks/${tasks[1].id}`, { + body: { + status: 'completed', + }, + }); + const result = await models.task.find({ + id: tasks[1].id, + }); + expect(result[0].status).to.equal('completed'); + }); + + it('Sufficient permissions: allows a user to edit a task if they have access to the task, and the entity and survey that are being linked to the task', async () => { + await app.grantAccess({ + DL: ['Donor'], + TO: ['Donor'], + }); + await app.put(`tasks/${tasks[1].id}`, { + body: { + survey_id: surveys[1].survey.id, + entity_id: facilities[1].id, + }, + }); + const result = await models.task.find({ + id: tasks[1].id, + }); + expect(result[0].entity_id).to.equal(facilities[1].id); + expect(result[0].survey_id).to.equal(surveys[1].survey.id); + }); + + it('Insufficient permissions: throws an error if the user does not have access to the task being edited', async () => { + await app.grantAccess(DEFAULT_POLICY); + const { body: result } = await app.put(`tasks/${tasks[0].id}`, { + body: { + status: 'completed', + }, + }); + expect(result).to.have.keys('error'); + expect(result.error).to.include('Need to have access to the country of the task'); + }); + + it('Insufficient permissions: throws an error if the user does not have access to the survey being linked to the task', async () => { + await app.grantAccess(DEFAULT_POLICY); + const { body: result } = await app.put(`tasks/${tasks[1].id}`, { + body: { + survey_id: surveys[0].survey.id, + }, + }); + expect(result).to.have.keys('error'); + expect(result.error).to.include('Need to have access to the new survey of the task'); + }); + + it('Insufficient permissions: throws an error if the user does not have access to the entity being linked to the task', async () => { + await app.grantAccess(DEFAULT_POLICY); + const { body: result } = await app.put(`tasks/${tasks[1].id}`, { + body: { + entity_id: facilities[0].id, + }, + }); + expect(result).to.have.keys('error'); + expect(result.error).to.include('Need to have access to the new entity of the task'); }); - expect(result[0].entity_id).to.equal(facilities[0].id); - expect(result[0].survey_id).to.equal(surveys[0].survey.id); }); - it('Sufficient permissions: allows a user to edit a task if they have access to the task, and entity and survey are not being updated', async () => { - await app.grantAccess(DEFAULT_POLICY); - await app.put(`tasks/${tasks[1].id}`, { - body: { - status: 'completed', - }, + describe('User generated comments', async () => { + it('Handles adding a comment when editing a task', async () => { + await app.grantAccess({ + DL: ['Donor'], + TO: ['Donor'], + }); + await app.put(`tasks/${tasks[1].id}`, { + body: { + survey_id: surveys[1].survey.id, + entity_id: facilities[1].id, + comment: 'This is a test comment', + }, + }); + const result = await models.task.find({ + id: tasks[1].id, + }); + expect(result[0].entity_id).to.equal(facilities[1].id); + expect(result[0].survey_id).to.equal(surveys[1].survey.id); + + const comment = await models.taskComment.findOne({ + task_id: tasks[1].id, + message: 'This is a test comment', + }); + expect(comment).not.to.be.null; }); - const result = await models.task.find({ - id: tasks[1].id, + + it('Handles adding a comment when no other edits are made', async () => { + await app.grantAccess({ + DL: ['Donor'], + TO: ['Donor'], + }); + await app.put(`tasks/${tasks[1].id}`, { + body: { + comment: 'This is a test comment', + }, + }); + const result = await models.task.find({ + id: tasks[1].id, + }); + expect(result[0].entity_id).to.equal(tasks[1].entity_id); + + const comment = await models.taskComment.findOne({ + task_id: tasks[1].id, + message: 'This is a test comment', + }); + expect(comment).not.to.be.null; }); - expect(result[0].status).to.equal('completed'); }); - it('Sufficient permissions: allows a user to edit a task if they have access to the task, and the entity and survey that are being linked to the task', async () => { - await app.grantAccess({ - DL: ['Donor'], - TO: ['Donor'], + describe('System generated comments', () => { + it('Adds a comment when the due date changes on a task', async () => { + await app.grantAccess({ + DL: ['Donor'], + TO: ['Donor'], + }); + await app.put(`tasks/${tasks[1].id}`, { + body: { + due_date: new Date('2021-11-30').toISOString(), + }, + }); + + const comment = await models.taskComment.findOne({ + task_id: tasks[1].id, + type: models.taskComment.types.System, + message: 'Changed due date from 31 December 21 to 30 November 21', + }); + expect(comment).not.to.be.null; }); - await app.put(`tasks/${tasks[1].id}`, { - body: { - survey_id: surveys[1].survey.id, - entity_id: facilities[1].id, - }, + + it('Adds a comment when the repeat schedule changes from not repeating to repeating on a task', async () => { + await app.grantAccess({ + DL: ['Donor'], + TO: ['Donor'], + }); + await app.put(`tasks/${tasks[1].id}`, { + body: { + // this is currently null when setting a task to repeat + due_date: null, + repeat_schedule: { + frequency: 'daily', + }, + }, + }); + + const repeatComment = await models.taskComment.findOne({ + task_id: tasks[1].id, + type: models.taskComment.types.System, + message: "Changed recurring task from Doesn't repeat to Daily", + }); + + const dueDateComment = await models.taskComment.findOne({ + task_id: tasks[1].id, + type: models.taskComment.types.System, + message: 'Changed due date from 31 December 21 to null', + }); + expect(repeatComment).not.to.be.null; + // should not add a comment about the due date changing in this case + expect(dueDateComment).to.be.null; }); - const result = await models.task.find({ - id: tasks[1].id, + + it('Adds a comment when the repeat schedule changes from repeating to not repeating on a task', async () => { + await app.grantAccess({ + DL: ['Donor'], + TO: ['Donor'], + }); + await app.put(`tasks/${tasks[1].id}`, { + body: { + repeat_schedule: { + frequency: 'daily', + }, + }, + }); + + await app.put(`tasks/${tasks[1].id}`, { + body: { + repeat_schedule: null, + }, + }); + + const comment = await models.taskComment.findOne({ + task_id: tasks[1].id, + type: models.taskComment.types.System, + message: "Changed recurring task from Daily to Doesn't repeat", + }); + expect(comment).not.to.be.null; }); - expect(result[0].entity_id).to.equal(facilities[1].id); - expect(result[0].survey_id).to.equal(surveys[1].survey.id); - }); - it('Insufficient permissions: throws an error if the user does not have access to the task being edited', async () => { - await app.grantAccess(DEFAULT_POLICY); - const { body: result } = await app.put(`tasks/${tasks[0].id}`, { - body: { - status: 'completed', - }, + it('Adds a comment when the status changes on a task', async () => { + await app.grantAccess({ + DL: ['Donor'], + TO: ['Donor'], + }); + await app.put(`tasks/${tasks[1].id}`, { + body: { + status: 'completed', + }, + }); + + const comment = await models.taskComment.findOne({ + task_id: tasks[1].id, + type: models.taskComment.types.System, + message: 'Changed status from To do to Completed', + }); + expect(comment).not.to.be.null; }); - expect(result).to.have.keys('error'); - expect(result.error).to.include('Need to have access to the country of the task'); - }); - it('Insufficient permissions: throws an error if the user does not have access to the survey being linked to the task', async () => { - await app.grantAccess(DEFAULT_POLICY); - const { body: result } = await app.put(`tasks/${tasks[1].id}`, { - body: { - survey_id: surveys[0].survey.id, - }, + it('Adds a comment when the assignee changes to Unassigned on a task', async () => { + await app.grantAccess({ + DL: ['Donor'], + TO: ['Donor'], + }); + await app.put(`tasks/${tasks[1].id}`, { + body: { + assignee_id: null, + }, + }); + + const comment = await models.taskComment.findOne({ + task_id: tasks[1].id, + type: models.taskComment.types.System, + message: 'Changed assignee from Peter Pan to Unassigned', + }); + expect(comment).not.to.be.null; }); - expect(result).to.have.keys('error'); - expect(result.error).to.include('Need to have access to the new survey of the task'); - }); - it('Insufficient permissions: throws an error if the user does not have access to the entity being linked to the task', async () => { - await app.grantAccess(DEFAULT_POLICY); - const { body: result } = await app.put(`tasks/${tasks[1].id}`, { - body: { - entity_id: facilities[0].id, - }, + it('Adds a comment when the assignee changes from unassigned to assigned on a task', async () => { + await app.grantAccess(BES_ADMIN_POLICY); + await app.put(`tasks/${tasks[0].id}`, { + body: { + assignee_id: assignee.id, + }, + }); + + const comment = await models.taskComment.findOne({ + task_id: tasks[0].id, + type: models.taskComment.types.System, + message: 'Changed assignee from Unassigned to Peter Pan', + }); + expect(comment).not.to.be.null; }); - expect(result).to.have.keys('error'); - expect(result.error).to.include('Need to have access to the new entity of the task'); }); it('Handles adding a comment when editing a task', async () => { diff --git a/packages/database/src/__tests__/changeHandlers/TaskCompletionHandler.test.js b/packages/database/src/__tests__/changeHandlers/TaskCompletionHandler.test.js index 33a0f75c97..2f760d85de 100644 --- a/packages/database/src/__tests__/changeHandlers/TaskCompletionHandler.test.js +++ b/packages/database/src/__tests__/changeHandlers/TaskCompletionHandler.test.js @@ -95,6 +95,12 @@ describe('TaskCompletionHandler', () => { { entity_id: tonga.id, date: '2024-07-21' }, ]); await assertTaskStatus(task.id, 'completed', responseIds[0]); + const comments = await models.taskComment.find({ task_id: task.id }); + expect(comments[0]).toMatchObject({ + message: 'Completed this task', + user_id: userId, + type: models.taskComment.types.System, + }); }); it('created response marks associated tasks as completed if created_time === data_time', async () => { @@ -106,6 +112,18 @@ describe('TaskCompletionHandler', () => { await createResponses([{ entity_id: tonga.id, date: '2021-07-08' }]); await assertTaskStatus(task.id, 'to_do', null); }); + + it('created response does not mark associated tasks as completed if status is null, but should still create a comment', async () => { + await models.task.update({ id: task.id }, { status: null }); + await createResponses([{ entity_id: tonga.id, date: '2021-07-08' }]); + await assertTaskStatus(task.id, null, null); + const comments = await models.taskComment.find({ task_id: task.id }); + expect(comments[0]).toMatchObject({ + message: 'Completed this task', + user_id: userId, + type: 'system', + }); + }); }); describe('updating a survey response', () => { diff --git a/packages/database/src/changeHandlers/TaskCompletionHandler.js b/packages/database/src/changeHandlers/TaskCompletionHandler.js index da135b0eb1..2c8dc5b9bf 100644 --- a/packages/database/src/changeHandlers/TaskCompletionHandler.js +++ b/packages/database/src/changeHandlers/TaskCompletionHandler.js @@ -47,6 +47,7 @@ export class TaskCompletionHandler extends ChangeHandler { ); return this.models.task.find({ + // only fetch tasks that have a status of 'to_do' or null (repeating tasks have a status of null) status: 'to_do', [QUERY_CONJUNCTIONS.OR]: { status: { @@ -67,7 +68,7 @@ export class TaskCompletionHandler extends ChangeHandler { }); } - async handleChanges(_transactingModels, changedResponses) { + async handleChanges(transactingModels, changedResponses) { // if there are no changed responses, we don't need to do anything if (changedResponses.length === 0) return; const tasksToUpdate = await this.fetchTasksForSurveyResponses(changedResponses); @@ -97,7 +98,7 @@ export class TaskCompletionHandler extends ChangeHandler { // Create a new task with the same details as the current task and mark as completed // It is theoretically possible that more than one task could be created for a repeating // task in a reporting period which is ok from a business point of view - await this.models.task.create({ + await transactingModels.task.create({ assignee_id: assigneeId, survey_id: surveyId, entity_id: entityId, @@ -105,13 +106,18 @@ export class TaskCompletionHandler extends ChangeHandler { status: 'completed', survey_response_id: matchingSurveyResponse.id, }); - continue; + } else { + await transactingModels.task.updateById(id, { + status: 'completed', + survey_response_id: matchingSurveyResponse.id, + }); } - await this.models.task.updateById(id, { - status: 'completed', - survey_response_id: matchingSurveyResponse.id, - }); + await task.addComment( + 'Completed this task', + matchingSurveyResponse.user_id, + transactingModels.taskComment.types.System, + ); } } } diff --git a/packages/database/src/modelClasses/Task.js b/packages/database/src/modelClasses/Task.js index 90759ec43a..411d03153f 100644 --- a/packages/database/src/modelClasses/Task.js +++ b/packages/database/src/modelClasses/Task.js @@ -9,6 +9,65 @@ import { DatabaseRecord } from '../DatabaseRecord'; import { RECORDS } from '../records'; import { JOIN_TYPES } from '../TupaiaDatabase'; +/** + * + * @description Get a human-friendly name for a field. In most cases, this just replaces underscores with spaces, but there are some special cases like 'assignee_id' and 'repeat_schedule' that need to be handled differently + * + * @param {string} field + * @returns {string} + */ +const getFriendlyFieldName = field => { + if (field === 'assignee_id') { + return 'assignee'; + } + if (field === 'repeat_schedule') { + return 'recurring task'; + } + + // Default to replacing underscores with spaces + return field.replace(/_/g, ' '); +}; + +/** + * @description Format the value of a field for display in a comment. This is used to make the comment more human-readable, and handles special cases like formatting dates and assignee names + * + * @param {string} field + * @param {*} value + * @param {*} models + * @returns {Promise} + */ +const formatValue = async (field, value, models) => { + switch (field) { + case 'assignee_id': { + if (!value) { + return 'Unassigned'; + } + const assignee = await models.user.findById(value); + return assignee.full_name; + } + case 'repeat_schedule': { + if (!value || !value?.frequency) { + return "Doesn't repeat"; + } + + return `${value.frequency.charAt(0).toUpperCase()}${value.frequency.slice(1)}`; + } + case 'due_date': { + // TODO: Currently repeating tasks don't have a due date, so we need to handle null values. In RN-1341 we will add a due date to repeating tasks overnight, so this will need to be updated then + if (!value) { + return 'No due date'; + } + // Format the date as 'd MMMM yy' (e.g. 1 January 21). This is so that there is no ambiguity between US and other date formats + return format(new Date(value), 'd MMMM yy'); + } + default: { + // Default to capitalizing the value's first character, and replacing underscores with spaces + const words = value.replace(/_/g, ' '); + return `${words.charAt(0).toUpperCase()}${words.slice(1)}`; + } + } +}; + export class TaskRecord extends DatabaseRecord { static databaseRecord = RECORDS.TASK; @@ -44,28 +103,99 @@ export class TaskRecord extends DatabaseRecord { return this.otherModels.survey.findById(this.survey_id); } + /** + * @description Get all comments for the task + * @returns {Promise} + */ + async comments() { return this.otherModels.taskComment.find({ task_id: this.id }); } + /** + * @description Get all user comments for the task + * @returns {Promise} + */ async userComments() { - return this.otherModels.taskComment.find({ task_id: this.id, type: 'user' }); + return this.otherModels.taskComment.find({ + task_id: this.id, + type: this.otherModels.taskComment.types.User, + }); } + /** + * @description Get all system comments for the task + * @returns {Promise} + */ + async systemComments() { - return this.otherModels.taskComment.find({ task_id: this.id, type: 'system' }); + return this.otherModels.taskComment.find({ + task_id: this.id, + type: this.otherModels.taskComment.types.System, + }); } + /** + * @description Add a comment to the task. Handles linking the comment to the task and user, and setting the comment type + * + * @param {string} message + * @param {string} userId + * @param {string} type + * + */ - async addComment(message, userId, type = 'user') { + async addComment(message, userId, type) { const user = await this.otherModels.user.findById(userId); return this.otherModels.taskComment.create({ - message, task_id: this.id, - user_id: userId, - user_name: user.full_name, + message, type, + user_id: userId, + user_name: user?.full_name ?? null, }); } + + /** + * @description Add system comments when a task is updated. This is used to automatically add comments when certain fields are updated, e.g. due date, assignee, etc. + * + * @param {object} updatedFields + * @param {string} userId + */ + + async addSystemCommentsOnUpdate(updatedFields, userId) { + const fieldsToCreateCommentsFor = ['due_date', 'repeat_schedule', 'status', 'assignee_id']; + const comments = []; + + // Loop through the updated fields and add a comment for each one that has changed + for (const [field, newValue] of Object.entries(updatedFields)) { + // Only create comments for certain fields + if (!fieldsToCreateCommentsFor.includes(field)) continue; + const originalValue = this[field]; + // If the field hasn't actually changed, don't add a comment + if (originalValue === newValue) continue; + + // If the due date is changed and the task is repeating, don't add a comment, because this just means that the repeat schedule is being updated, not that the due date is being changed. This will likely change as part of RN-1341 + // TODO: re-evaulate this when RN-1341 is implemented + if (field === 'due_date' && updatedFields.repeat_schedule) { + continue; + } + + const friendlyFieldName = getFriendlyFieldName(field); + const formattedOriginalValue = await formatValue(field, originalValue, this.otherModels); + const formattedNewValue = await formatValue(field, newValue, this.otherModels); + + comments.push( + `Changed ${friendlyFieldName} from ${formattedOriginalValue} to ${formattedNewValue}`, + ); + } + + if (!comments.length) return; + + await Promise.all( + comments.map(message => + this.addComment(message, userId, this.otherModels.taskComment.types.System), + ), + ); + } } export class TaskModel extends DatabaseModel { diff --git a/packages/database/src/modelClasses/TaskComment.js b/packages/database/src/modelClasses/TaskComment.js index 2f471f41a8..3accc2dbd7 100644 --- a/packages/database/src/modelClasses/TaskComment.js +++ b/packages/database/src/modelClasses/TaskComment.js @@ -15,4 +15,9 @@ export class TaskCommentModel extends DatabaseModel { get DatabaseRecordClass() { return TaskCommentRecord; } + + types = { + System: 'system', + User: 'user', + }; } diff --git a/packages/datatrak-web-server/src/routes/CreateTaskRoute.ts b/packages/datatrak-web-server/src/routes/CreateTaskRoute.ts index c03306c6a9..38b847f4bc 100644 --- a/packages/datatrak-web-server/src/routes/CreateTaskRoute.ts +++ b/packages/datatrak-web-server/src/routes/CreateTaskRoute.ts @@ -19,7 +19,7 @@ export class CreateTaskRoute extends Route { public async buildResponse() { const { models, body, ctx } = this.req; - const { surveyCode, entityId, comment } = body; + const { survey_code: surveyCode } = body; const survey = await models.survey.findOne({ code: surveyCode }); if (!survey) { @@ -29,13 +29,16 @@ export class CreateTaskRoute extends Route { const taskDetails = formatTaskChanges({ ...body, survey_id: survey.id, - entity_id: entityId, }); if (taskDetails.due_date) { taskDetails.status = TaskStatus.to_do; } - return ctx.services.central.createResource('tasks', {}, taskDetails); + await ctx.services.central.createResource('tasks', {}, taskDetails); + + return { + message: 'Task created successfully', + }; } } diff --git a/packages/datatrak-web-server/src/utils/formatTaskChanges.ts b/packages/datatrak-web-server/src/utils/formatTaskChanges.ts index b03437c352..fb475e1328 100644 --- a/packages/datatrak-web-server/src/utils/formatTaskChanges.ts +++ b/packages/datatrak-web-server/src/utils/formatTaskChanges.ts @@ -15,18 +15,16 @@ type Output = Partial> & { }; export const formatTaskChanges = (task: Input) => { - const { dueDate, repeatSchedule, assigneeId, ...restOfTask } = task; + const { due_date: dueDate, repeat_schedule: repeatSchedule, ...restOfTask } = task; - const taskDetails: Output & { - due_date?: string | null; - } = { assignee_id: assigneeId, ...restOfTask }; + const taskDetails: Output = restOfTask; if (repeatSchedule) { // if task is repeating, clear due date - taskDetails.repeat_schedule = JSON.stringify({ + taskDetails.repeat_schedule = { // TODO: format this correctly when recurring tasks are implemented frequency: repeatSchedule, - }); + }; taskDetails.due_date = null; } else if (dueDate) { // apply status and due date only if not a repeating task diff --git a/packages/datatrak-web/src/features/Tasks/CreateTaskModal/CreateTaskModal.tsx b/packages/datatrak-web/src/features/Tasks/CreateTaskModal/CreateTaskModal.tsx index 44316faf56..435f70c225 100644 --- a/packages/datatrak-web/src/features/Tasks/CreateTaskModal/CreateTaskModal.tsx +++ b/packages/datatrak-web/src/features/Tasks/CreateTaskModal/CreateTaskModal.tsx @@ -104,11 +104,11 @@ export const CreateTaskModal = ({ onClose }: CreateTaskModalProps) => { const defaultDueDate = generateDefaultDueDate(); const defaultValues = { - surveyCode: null, - entityId: null, - dueDate: defaultDueDate, - repeatSchedule: null, - assigneeId: null, + survey_code: null, + entity_id: null, + due_date: defaultDueDate, + repeat_schedule: null, + assignee_id: null, }; const formContext = useForm({ mode: 'onChange', @@ -164,18 +164,18 @@ export const CreateTaskModal = ({ onClose }: CreateTaskModalProps) => { useEffect(() => { if (!selectedCountry?.code) return; - const { surveyCode, entityId } = dirtyFields; + const { survey_code: surveyCode, entity_id: entityId } = dirtyFields; // reset surveyCode and entityId when country changes, if they are dirty if (surveyCode) { - setValue('surveyCode', null, { shouldValidate: true }); + setValue('survey_code', null, { shouldValidate: true }); } if (entityId) { - setValue('entityId', null, { shouldValidate: true }); + setValue('entity_id', null, { shouldValidate: true }); } }, [selectedCountry?.code]); - const surveyCode = watch('surveyCode'); - const dueDate = watch('dueDate'); + const surveyCode = watch('survey_code'); + const dueDate = watch('due_date'); return ( { /> ( @@ -220,7 +220,7 @@ export const CreateTaskModal = ({ onClose }: CreateTaskModalProps) => { )} /> { @@ -241,7 +241,7 @@ export const CreateTaskModal = ({ onClose }: CreateTaskModalProps) => { /> { }} /> ( @@ -273,7 +273,7 @@ export const CreateTaskModal = ({ onClose }: CreateTaskModalProps) => { ( { - const { createdAt, userName, message } = comment; + const { createdAt, userName, message, type } = comment; return ( {displayDateTime(createdAt)} - {userName} - {message} + + {message} + ); }; diff --git a/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskDetails.tsx b/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskDetails.tsx index dccdc3c8e5..8e25a9d745 100644 --- a/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskDetails.tsx +++ b/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskDetails.tsx @@ -98,9 +98,9 @@ export const TaskDetails = ({ task }: { task: SingleTaskResponse }) => { const backLink = useFromLocation(); const defaultValues = { - dueDate: task.dueDate ?? null, - repeatSchedule: task.repeatSchedule?.frequency ?? null, - assigneeId: task.assigneeId ?? null, + due_date: task.dueDate ?? null, + repeat_schedule: task.repeatSchedule?.frequency ?? null, + assignee_id: task.assigneeId ?? null, }; const formContext = useForm({ mode: 'onChange', @@ -111,7 +111,7 @@ export const TaskDetails = ({ task }: { task: SingleTaskResponse }) => { handleSubmit, watch, register, - formState: { isValid, dirtyFields }, + formState: { dirtyFields }, reset, } = formContext; @@ -130,10 +130,19 @@ export const TaskDetails = ({ task }: { task: SingleTaskResponse }) => { const canEditFields = task.taskStatus !== TaskStatus.completed && task.taskStatus !== TaskStatus.cancelled; - const dueDate = watch('dueDate'); + const dueDate = watch('due_date'); + + const onSubmit = data => { + const updatedFields = Object.keys(dirtyFields).reduce((acc, key) => { + acc[key] = data[key]; + return acc; + }, {}); + + editTask(updatedFields); + }; return ( -
+ @@ -142,10 +151,9 @@ export const TaskDetails = ({ task }: { task: SingleTaskResponse }) => { ( { ( { ( { Clear changes - diff --git a/packages/types/src/types/requests/datatrak-web-server/TaskChangeRequest.ts b/packages/types/src/types/requests/datatrak-web-server/TaskChangeRequest.ts index d6551b6348..8dbe85d47b 100644 --- a/packages/types/src/types/requests/datatrak-web-server/TaskChangeRequest.ts +++ b/packages/types/src/types/requests/datatrak-web-server/TaskChangeRequest.ts @@ -3,18 +3,14 @@ * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd */ -import { Entity, Survey, UserAccount } from '../../models'; +import { Survey, Task } from '../../models'; export type Params = Record; export type ResBody = { message: string; }; export type ReqQuery = Record; -export type ReqBody = { - assigneeId?: UserAccount['id']; - dueDate?: string; - entityId: Entity['id']; - repeatSchedule?: string; - surveyCode: Survey['code']; +export type ReqBody = Partial> & { + survey_code: Survey['code']; comment?: string; };