From bd3d86a6dd67e8455618e00c8b7a41b4dec0e24c Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 8 Apr 2024 13:26:37 -0400 Subject: [PATCH] - Tasks Audit Trail Additions Signed-off-by: Dave Richer --- .../task-list/task-list.container.jsx | 50 +++++++++++++--- .../task-upsert-modal.container.jsx | 60 ++++++++++++------- client/src/graphql/tasks.queries.js | 4 ++ client/src/translations/en_us/common.json | 8 ++- client/src/translations/es/common.json | 8 ++- client/src/translations/fr/common.json | 8 ++- client/src/utils/AuditTrailMappings.js | 29 ++++++++- 7 files changed, 134 insertions(+), 33 deletions(-) diff --git a/client/src/components/task-list/task-list.container.jsx b/client/src/components/task-list/task-list.container.jsx index 501bad527..709da4a9b 100644 --- a/client/src/components/task-list/task-list.container.jsx +++ b/client/src/components/task-list/task-list.container.jsx @@ -11,6 +11,9 @@ import React, {useEffect} from "react"; import TaskListComponent from "./task-list.component.jsx"; import {notification} from "antd"; import {useTranslation} from "react-i18next"; +import {useDispatch} from "react-redux"; +import {insertAuditTrail} from "../../redux/application/application.actions.js"; +import AuditTrailMapping from "../../utils/AuditTrailMappings.js"; export default function TaskListContainer({ bodyshop, @@ -25,6 +28,7 @@ export default function TaskListContainer({ const {t} = useTranslation(); const searchParams = queryString.parse(useLocation().search); const {page, sortcolumn, sortorder, deleted, completed, mine} = searchParams; + const dispatch = useDispatch(); const {loading, error, data, refetch} = useQuery( query, { @@ -80,16 +84,30 @@ export default function TaskListContainer({ const toggleCompletedStatus = async (id, currentStatus) => { const completed_at = !currentStatus ? new Date().toISOString() : null; try { - await toggleTaskCompleted({ + const toggledTask = await toggleTaskCompleted({ variables: { id: id, completed: !currentStatus, completed_at: completed_at } }); - // refetch().catch((e) => { - // console.error(`Something went wrong fetching tasks: ${e.message || ''}`); - // }); + + if (!toggledTask.errors) { + dispatch( + insertAuditTrail({ + jobid: toggledTask.data.update_tasks_by_pk.jobid, + operation: toggledTask?.data?.update_tasks_by_pk?.completed ? AuditTrailMapping.tasksCompleted( + toggledTask.data.update_tasks_by_pk.title, + currentUser.email + ) : AuditTrailMapping.tasksUncompleted( + toggledTask.data.update_tasks_by_pk.title, + currentUser.email + ), + type: toggledTask?.data?.update_tasks_by_pk?.completed ? "tasksCompleted" : "tasksUncompleted" + }) + ) + } + window.dispatchEvent(new CustomEvent('taskUpdated', { detail: {message: 'A task has been completed.'}, })); @@ -114,26 +132,42 @@ export default function TaskListContainer({ * @param currentStatus * @returns {Promise} */ + const toggleDeletedStatus = async (id, currentStatus) => { const deleted_at = !currentStatus ? new Date().toISOString() : null; try { - await toggleTaskDeleted({ + const toggledTask = await toggleTaskDeleted({ variables: { id: id, deleted: !currentStatus, deleted_at: deleted_at } }); + if (!toggledTask.errors) { + dispatch( + insertAuditTrail({ + jobid: toggledTask.data.update_tasks_by_pk.jobid, + operation: toggledTask?.data?.update_tasks_by_pk?.deleted ? AuditTrailMapping.tasksDeleted( + toggledTask.data.update_tasks_by_pk.title, + currentUser.email + ) : AuditTrailMapping.tasksUndeleted( + toggledTask.data.update_tasks_by_pk.title, + currentUser.email + ), + type: toggledTask?.data?.update_tasks_by_pk?.deleted ? "tasksDeleted" : "tasksUndeleted" + }) + ) + } + + window.dispatchEvent(new CustomEvent('taskUpdated', { detail: {message: 'A task has been deleted.'}, })); - // refetch().catch((e) => { - // console.error(`Something went wrong fetching tasks: ${e.message || ''}`); - // }); notification["success"]({ message: t("tasks.successes.deleted"), }); } catch (err) { + console.dir(err); notification["error"]({ message: t("tasks.failures.deleted"), }); diff --git a/client/src/components/task-upsert-modal/task-upsert-modal.container.jsx b/client/src/components/task-upsert-modal/task-upsert-modal.container.jsx index e792b0c0c..91fae5bef 100644 --- a/client/src/components/task-upsert-modal/task-upsert-modal.container.jsx +++ b/client/src/components/task-upsert-modal/task-upsert-modal.container.jsx @@ -18,6 +18,8 @@ import {replaceUndefinedWithNull} from "../../utils/undefinedtonull.js"; import {useNavigate} from "react-router-dom"; import axios from "axios"; import dayjs from '../../utils/day'; +import {insertAuditTrail} from "../../redux/application/application.actions.js"; +import AuditTrailMapping from "../../utils/AuditTrailMappings.js"; const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser, @@ -26,6 +28,8 @@ const mapStateToProps = createStructuredSelector({ }); const mapDispatchToProps = (dispatch) => ({ toggleModalVisible: () => dispatch(toggleModalVisible("taskUpsert")), + insertAuditTrail: ({ jobid, billid, operation, type }) => + dispatch(insertAuditTrail({ jobid, billid, operation, type })) }); export function TaskUpsertModalContainer({ @@ -33,6 +37,7 @@ export function TaskUpsertModalContainer({ currentUser, taskUpsert, toggleModalVisible, + insertAuditTrail }) { const {t} = useTranslation(); const history = useNavigate(); @@ -123,14 +128,28 @@ export function TaskUpsertModalContainer({ const handleExistingTask = async (values) => { const isAssignedToDirty = values.assigned_to !== existingTask.assigned_to; - await updateTask({ + const taskData = await updateTask({ variables: { taskId: existingTask.id, task: replaceUndefinedWithNull(values) }, }); + + if (!taskData.errors) { + const oldTask = taskData?.data?.update_tasks?.returning[0]; + insertAuditTrail({ + jobid: oldTask.jobid, + operation: AuditTrailMapping.tasksUpdated( + oldTask.title, + currentUser.email + ), + type: "tasksUpdated" + }); + } + if (isAssignedToDirty) { + // TODO This is being moved serverside axios.post("/sendemail", { from: { name: bodyshop.shopname, @@ -163,7 +182,7 @@ export function TaskUpsertModalContainer({ }; const handleNewTask = async (values) => { - const newTaskID = (await insertTask({ + const newTaskData = await insertTask({ variables: { taskInput: [ { @@ -173,30 +192,29 @@ export function TaskUpsertModalContainer({ }, ], }, - // TODO: Consult Patrick, because this fails on relationship data, and an event emitter is just much easier to use - // update(cache) { - // cache.modify({ - // fields: { - // tasks(existingTasks) { - // return [{ - // ...values, - // jobid: selectedJobId || values.jobid, - // created_by: currentUser.email, - // bodyshopid: bodyshop.id - // }, ...existingTasks] - // }, - // }, - // }); - // }, - })).data.insert_tasks.returning[0].id; - + }); + + const newTask = newTaskData?.data?.insert_tasks?.returning[0]; + const newTaskID = newTask?.id; + + if (!newTaskData.errors) { + insertAuditTrail({ + jobid: newTask.jobid, + operation: AuditTrailMapping.tasksCreated( + newTask.title, + currentUser.email + ), + type: "tasksCreated" + }); + } + if (refetch) await refetch(); form.resetFields(); - toggleModalVisible(); - + // send notification to the assigned user + // TODO: This is being moved serverside axios.post("/sendemail", { from: { name: bodyshop.shopname, diff --git a/client/src/graphql/tasks.queries.js b/client/src/graphql/tasks.queries.js index 78f27a449..cc6ef9112 100644 --- a/client/src/graphql/tasks.queries.js +++ b/client/src/graphql/tasks.queries.js @@ -330,8 +330,10 @@ export const MUTATION_TOGGLE_TASK_COMPLETED = gql` mutation MUTATION_TOGGLE_TASK_COMPLETED($id: uuid!, $completed: Boolean!, $completed_at: timestamptz) { update_tasks_by_pk(pk_columns: {id: $id}, _set: {completed: $completed, completed_at: $completed_at}) { id + title completed completed_at + jobid } } `; @@ -344,8 +346,10 @@ export const MUTATION_TOGGLE_TASK_DELETED = gql` mutation MUTATION_TOGGLE_TASK_DELETED($id: uuid!, $deleted: Boolean!, $deleted_at: timestamptz) { update_tasks_by_pk(pk_columns: {id: $id}, _set: {deleted: $deleted, deleted_at: $deleted_at}) { id + title deleted deleted_at + jobid } } `; diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index d4daeaedc..e7e2b85a4 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -134,7 +134,13 @@ "jobstatuschange": "Job status changed to {{status}}.", "jobsupplement": "Job supplement imported.", "jobsuspend": "Suspend Toggle set to {{status}}", - "jobvoid": "Job has been voided." + "jobvoid": "Job has been voided.", + "tasks_created": "Task '{{title}}' created by {{createdBy}}", + "tasks_updated": "Task '{{title}}' updated by {{updatedBy}}", + "tasks_deleted": "Task '{{title}}' deleted by {{deletedBy}}", + "tasks_undeleted": "Task '{{title}}' undeleted by {{undeletedBy}}", + "tasks_completed": "Task '{{title}}' completed by {{completedBy}}", + "tasks_uncompleted": "Task '{{title}}' uncompleted by {{uncompletedBy}}" } }, "billlines": { diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 26e5c69e7..46a2b6cd7 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -134,7 +134,13 @@ "jobstatuschange": "", "jobsupplement": "", "jobsuspend": "", - "jobvoid": "" + "jobvoid": "", + "tasks_created": "", + "tasks_updated": "", + "tasks_deleted": "", + "tasks_undeleted": "", + "tasks_completed": "", + "tasks_uncompleted": "" } }, "billlines": { diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 21bedcf52..e2703dd1b 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -134,7 +134,13 @@ "jobstatuschange": "", "jobsupplement": "", "jobsuspend": "", - "jobvoid": "" + "jobvoid": "", + "tasks_created": "", + "tasks_updated": "", + "tasks_deleted": "", + "tasks_undeleted": "", + "tasks_completed": "", + "tasks_uncompleted": "" } }, "billlines": { diff --git a/client/src/utils/AuditTrailMappings.js b/client/src/utils/AuditTrailMappings.js index f2de52319..b0f8e0924 100644 --- a/client/src/utils/AuditTrailMappings.js +++ b/client/src/utils/AuditTrailMappings.js @@ -39,7 +39,34 @@ const AuditTrailMapping = { jobstatuschange: (status) => i18n.t("audit_trail.messages.jobstatuschange", { status }), jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"), jobsuspend: (status) => i18n.t("audit_trail.messages.jobsuspend", { status }), - jobvoid: () => i18n.t("audit_trail.messages.jobvoid") + jobvoid: () => i18n.t("audit_trail.messages.jobvoid"), +// Tasks Entries + tasksCreated: (title, createdBy) => i18n.t("audit_trail.messages.tasks_created", { + title, + createdBy + }), + tasksUpdated: (title, updatedBy) => i18n.t("audit_trail.messages.tasks_updated", { + title, + updatedBy + }), + tasksDeleted: (title, deletedBy) => i18n.t("audit_trail.messages.tasks_deleted", { + title, + deletedBy + }), + tasksUndeleted: (title, undeletedBy) => i18n.t("audit_trail.messages.tasks_undeleted", { + title, + undeletedBy + }), + tasksCompleted: (title, completedBy) => i18n.t("audit_trail.messages.tasks_completed", { + title, + completedBy + }), + tasksUncompleted: (title, uncompletedBy) => i18n.t("audit_trail.messages.tasks_uncompleted", { + title, + uncompletedBy + }), + + }; export default AuditTrailMapping;