Merged in feature/IO-2677-Tasks (pull request #1427)

Feature/IO-2677 Tasks

Approved-by: Patrick Fic
This commit is contained in:
Allan Carr
2024-04-19 16:29:32 +00:00
committed by Patrick Fic
38 changed files with 165 additions and 60 deletions

View File

@@ -236,6 +236,7 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
<EmployeeSearchSelect options={employeeData ? employeeData.employees : []} /> <EmployeeSearchSelect options={employeeData ? employeeData.employees : []} />
</Form.Item> </Form.Item>
); );
//This was introduced with tasks before assigned_to was shifted to UUID. Keeping in place for reference in the future if needed.
if (idtype === "employeeWithEmail") if (idtype === "employeeWithEmail")
return ( return (
<Form.Item <Form.Item

View File

@@ -149,7 +149,7 @@ function TaskListComponent({
sorter: true, sorter: true,
sortOrder: sortcolumn === "assigned_to" && sortorder, sortOrder: sortcolumn === "assigned_to" && sortorder,
render: (text, record) => { render: (text, record) => {
const employee = bodyshop?.employees?.find((e) => e.user_email === record.assigned_to); const employee = bodyshop?.employees?.find((e) => e.id === record.assigned_to);
return employee ? `${employee.first_name} ${employee.last_name}` : t("general.labels.na"); return employee ? `${employee.first_name} ${employee.last_name}` : t("general.labels.na");
} }
}); });

View File

@@ -48,8 +48,8 @@ export function TaskListContainer({
bodyshop: bodyshop.id, bodyshop: bodyshop.id,
[relationshipType]: relationshipId, [relationshipType]: relationshipId,
deleted: deleted === "true", deleted: deleted === "true",
completed: completed === "true", completed: completed === "true", //TODO: Find where mine is set.
assigned_to: mine === "true" ? currentUser.email : undefined, // replace currentUserID with the actual ID of the current user assigned_to: onlyMine ? bodyshop?.employees?.find((e) => e.user_email === currentUser.email)?.id : undefined, // replace currentUserID with the actual ID of the current user
offset: page ? (page - 1) * pageLimit : 0, offset: page ? (page - 1) * pageLimit : 0,
limit: pageLimit, limit: pageLimit,
order: [ order: [

View File

@@ -224,9 +224,7 @@ export function TaskUpsertModalComponent({
label={t("tasks.fields.assigned_to")} label={t("tasks.fields.assigned_to")}
name="assigned_to" name="assigned_to"
initialValue={ initialValue={
bodyshop.employees.find((employee) => employee?.user_email === currentUser.email && employee.active) bodyshop.employees.find((employee) => employee?.user_email === currentUser.email && employee.active)?.id
? currentUser.email
: undefined
} }
rules={[ rules={[
{ {
@@ -240,7 +238,7 @@ export function TaskUpsertModalComponent({
.filter((x) => x.active && x.user_email) .filter((x) => x.active && x.user_email)
.map((employee) => ({ .map((employee) => ({
key: employee.id, key: employee.id,
value: employee.user_email, value: employee.id,
label: `${employee.first_name} ${employee.last_name}` label: `${employee.first_name} ${employee.last_name}`
}))} }))}
/> />

View File

@@ -12,6 +12,10 @@ export const PARTIAL_TASK_FIELDS = gql`
due_date due_date
created_by created_by
assigned_to assigned_to
assigned_to_employee {
id
user_email
}
completed completed
completed_at completed_at
remind_at remind_at
@@ -80,7 +84,7 @@ export const QUERY_ALL_TASKS_PAGINATED = gql`
$bodyshop: uuid! $bodyshop: uuid!
$deleted: Boolean $deleted: Boolean
$completed: Boolean $completed: Boolean
$assigned_to: String $assigned_to: uuid
$order: [tasks_order_by!]! $order: [tasks_order_by!]!
) { ) {
tasks( tasks(
@@ -121,7 +125,7 @@ export const QUERY_JOBLINE_TASKS_PAGINATED = gql`
$bodyshop: uuid! $bodyshop: uuid!
$deleted: Boolean $deleted: Boolean
$completed: Boolean $completed: Boolean
$assigned_to: String $assigned_to: uuid
$order: [tasks_order_by!]! $order: [tasks_order_by!]!
) { ) {
tasks( tasks(
@@ -164,7 +168,7 @@ export const QUERY_PARTSORDER_TASKS_PAGINATED = gql`
$bodyshop: uuid! $bodyshop: uuid!
$deleted: Boolean $deleted: Boolean
$completed: Boolean $completed: Boolean
$assigned_to: String $assigned_to: uuid
$order: [tasks_order_by!]! $order: [tasks_order_by!]!
) { ) {
tasks( tasks(
@@ -207,7 +211,7 @@ export const QUERY_BILL_TASKS_PAGINATED = gql`
$bodyshop: uuid! $bodyshop: uuid!
$deleted: Boolean $deleted: Boolean
$completed: Boolean $completed: Boolean
$assigned_to: String $assigned_to: uuid
$order: [tasks_order_by!]! $order: [tasks_order_by!]!
) { ) {
tasks( tasks(
@@ -250,7 +254,7 @@ export const QUERY_JOB_TASKS_PAGINATED = gql`
$bodyshop: uuid! $bodyshop: uuid!
$deleted: Boolean $deleted: Boolean
$completed: Boolean $completed: Boolean
$assigned_to: String $assigned_to: uuid
$order: [tasks_order_by!]! $order: [tasks_order_by!]!
) { ) {
tasks( tasks(
@@ -288,7 +292,7 @@ export const QUERY_MY_TASKS_PAGINATED = gql`
query QUERY_MY_TASKS_PAGINATED( query QUERY_MY_TASKS_PAGINATED(
$offset: Int $offset: Int
$limit: Int $limit: Int
$user: String! $assigned_to: uuid!
$bodyshop: uuid! $bodyshop: uuid!
$deleted: Boolean $deleted: Boolean
$completed: Boolean $completed: Boolean
@@ -299,7 +303,7 @@ export const QUERY_MY_TASKS_PAGINATED = gql`
limit: $limit limit: $limit
order_by: $order order_by: $order
where: { where: {
assigned_to: { _eq: $user } assigned_to: { _eq: $assigned_to }
bodyshopid: { _eq: $bodyshop } bodyshopid: { _eq: $bodyshop }
deleted: { _eq: $deleted } deleted: { _eq: $deleted }
completed: { _eq: $completed } completed: { _eq: $completed }
@@ -309,7 +313,7 @@ export const QUERY_MY_TASKS_PAGINATED = gql`
} }
tasks_aggregate( tasks_aggregate(
where: { where: {
assigned_to: { _eq: $user } assigned_to: { _eq: $assigned_to }
bodyshopid: { _eq: $bodyshop } bodyshopid: { _eq: $bodyshop }
deleted: { _eq: $deleted } deleted: { _eq: $deleted }
completed: { _eq: $completed } completed: { _eq: $completed }

View File

@@ -3,24 +3,25 @@ import TaskListContainer from "../../components/task-list/task-list.container.js
import { QUERY_ALL_TASKS_PAGINATED, QUERY_MY_TASKS_PAGINATED } from "../../graphql/tasks.queries.js"; import { QUERY_ALL_TASKS_PAGINATED, QUERY_MY_TASKS_PAGINATED } from "../../graphql/tasks.queries.js";
import taskPageTypes from "./taskPageTypes.jsx"; import taskPageTypes from "./taskPageTypes.jsx";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectCurrentUser } from "../../redux/user/user.selectors.js"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
import { connect } from "react-redux"; import { connect } from "react-redux";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser currentUser: selectCurrentUser,
bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({}); const mapDispatchToProps = (dispatch) => ({});
export default connect(mapStateToProps, mapDispatchToProps)(TasksPageComponent); export default connect(mapStateToProps, mapDispatchToProps)(TasksPageComponent);
export function TasksPageComponent({ currentUser, type }) { export function TasksPageComponent({ bodyshop, currentUser, type }) {
switch (type) { switch (type) {
case taskPageTypes.MY_TASKS: case taskPageTypes.MY_TASKS:
return ( return (
<TaskListContainer <TaskListContainer
onlyMine={true} onlyMine={true}
relationshipId={currentUser.email} relationshipId={bodyshop?.employees?.find((e) => e.user_email === currentUser.email)?.id}
relationshipType={"user"} relationshipType={"assigned_to"}
query={{ QUERY_MY_TASKS_PAGINATED }} query={{ QUERY_MY_TASKS_PAGINATED }}
titleTranslation={"tasks.titles.my_tasks"} titleTranslation={"tasks.titles.my_tasks"}
disableJobRefetch={true} disableJobRefetch={true}

View File

@@ -2114,7 +2114,7 @@ export const TemplateList = (type, context) => {
title: i18n.t("reportcenter.templates.tasks_date_employee"), title: i18n.t("reportcenter.templates.tasks_date_employee"),
subject: i18n.t("reportcenter.templates.tasks_date_employee"), subject: i18n.t("reportcenter.templates.tasks_date_employee"),
key: "tasks_date_employee", key: "tasks_date_employee",
idtype: "employeeWithEmail", idtype: "employee",
disabled: false, disabled: false,
rangeFilter: { rangeFilter: {
object: i18n.t("reportcenter.labels.objects.tasks"), object: i18n.t("reportcenter.labels.objects.tasks"),

View File

@@ -2335,6 +2335,13 @@
table: table:
name: jobs name: jobs
schema: public schema: public
- name: tasks
using:
foreign_key_constraint_on:
column: assigned_to
table:
name: tasks
schema: public
- name: timetickets - name: timetickets
using: using:
foreign_key_constraint_on: foreign_key_constraint_on:
@@ -5678,6 +5685,9 @@
name: tasks name: tasks
schema: public schema: public
object_relationships: object_relationships:
- name: assigned_to_employee
using:
foreign_key_constraint_on: assigned_to
- name: bill - name: bill
using: using:
foreign_key_constraint_on: billid foreign_key_constraint_on: billid
@@ -5693,9 +5703,6 @@
- name: parts_order - name: parts_order
using: using:
foreign_key_constraint_on: partsorderid foreign_key_constraint_on: partsorderid
- name: user
using:
foreign_key_constraint_on: assigned_to
- name: userByCreatedBy - name: userByCreatedBy
using: using:
foreign_key_constraint_on: created_by foreign_key_constraint_on: created_by
@@ -5736,25 +5743,26 @@
- role: user - role: user
permission: permission:
columns: columns:
- completed
- deleted
- priority
- assigned_to - assigned_to
- created_by
- description
- title
- completed_at
- created_at
- deleted_at
- due_date
- remind_at
- updated_at
- billid - billid
- bodyshopid - bodyshopid
- completed
- completed_at
- created_at
- created_by
- deleted
- deleted_at
- description
- due_date
- id - id
- jobid - jobid
- joblineid - joblineid
- partsorderid - partsorderid
- priority
- remind_at
- remind_at_sent
- title
- updated_at
filter: filter:
bodyshop: bodyshop:
associations: associations:
@@ -6191,6 +6199,13 @@
table: table:
name: email_audit_trail name: email_audit_trail
schema: public schema: public
- name: employees
using:
foreign_key_constraint_on:
column: user_email
table:
name: employees
schema: public
- name: eula_acceptances - name: eula_acceptances
using: using:
foreign_key_constraint_on: foreign_key_constraint_on:
@@ -6240,13 +6255,6 @@
table: table:
name: parts_orders name: parts_orders
schema: public schema: public
- name: tasks
using:
foreign_key_constraint_on:
column: assigned_to
table:
name: tasks
schema: public
- name: tasksByCreatedBy - name: tasksByCreatedBy
using: using:
foreign_key_constraint_on: foreign_key_constraint_on:

View File

@@ -0,0 +1,2 @@
alter table "public"."employees" drop constraint "employees_user_email_shopid_key";
alter table "public"."employees" add constraint "employees_user_email_key" unique ("user_email");

View File

@@ -0,0 +1,2 @@
alter table "public"."employees" drop constraint "employees_user_email_key";
alter table "public"."employees" add constraint "employees_user_email_shopid_key" unique ("user_email", "shopid");

View File

@@ -0,0 +1 @@
alter table "public"."tasks" drop constraint "tasks_created_by_fkey2";

View File

@@ -0,0 +1,5 @@
alter table "public"."tasks"
add constraint "tasks_created_by_fkey2"
foreign key ("created_by")
references "public"."users"
("email") on update restrict on delete restrict;

View File

@@ -0,0 +1,5 @@
alter table "public"."tasks"
add constraint "tasks_created_by_fkey2"
foreign key ("created_by")
references "public"."users"
("email") on update restrict on delete restrict;

View File

@@ -0,0 +1 @@
alter table "public"."tasks" drop constraint "tasks_created_by_fkey2";

View File

@@ -0,0 +1,5 @@
alter table "public"."tasks"
add constraint "tasks_assigned_to_fkey"
foreign key ("assigned_to")
references "public"."users"
("email") on update restrict on delete restrict;

View File

@@ -0,0 +1 @@
alter table "public"."tasks" drop constraint "tasks_assigned_to_fkey";

View File

@@ -0,0 +1,2 @@
alter table "public"."tasks" alter column "assigned_to" drop not null;
alter table "public"."tasks" add column "assigned_to" text;

View File

@@ -0,0 +1 @@
alter table "public"."tasks" drop column "assigned_to" cascade;

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."tasks" add column "assigned_to" uuid
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."tasks" add column "assigned_to" uuid
null;

View File

@@ -0,0 +1 @@
alter table "public"."tasks" drop constraint "tasks_assigned_to_fkey";

View File

@@ -0,0 +1,5 @@
alter table "public"."tasks"
add constraint "tasks_assigned_to_fkey"
foreign key ("assigned_to")
references "public"."employees"
("id") on update restrict on delete restrict;

View File

@@ -0,0 +1 @@
DROP INDEX IF EXISTS "public"."tasks_jobid";

View File

@@ -0,0 +1,2 @@
CREATE INDEX "tasks_jobid" on
"public"."tasks" using btree ("jobid");

View File

@@ -0,0 +1 @@
DROP INDEX IF EXISTS "public"."tasks_assigned-to";

View File

@@ -0,0 +1,2 @@
CREATE INDEX "tasks_assigned-to" on
"public"."tasks" using btree ("assigned_to");

View File

@@ -0,0 +1 @@
DROP INDEX IF EXISTS "public"."tasks_joblineid";

View File

@@ -0,0 +1,2 @@
CREATE INDEX "tasks_joblineid" on
"public"."tasks" using btree ("joblineid");

View File

@@ -0,0 +1 @@
DROP INDEX IF EXISTS "public"."tasks_partsorderid";

View File

@@ -0,0 +1,2 @@
CREATE INDEX "tasks_partsorderid" on
"public"."tasks" using btree ("partsorderid");

View File

@@ -0,0 +1 @@
DROP INDEX IF EXISTS "public"."tasks_remind_at";

View File

@@ -0,0 +1,2 @@
CREATE INDEX "tasks_remind_at" on
"public"."tasks" using btree ("remind_at");

View File

@@ -0,0 +1 @@
DROP INDEX IF EXISTS "public"."tasks_remind_at_sent";

View File

@@ -0,0 +1,2 @@
CREATE INDEX "tasks_remind_at_sent" on
"public"."tasks" using btree ("remind_at_sent");

View File

@@ -0,0 +1 @@
DROP INDEX IF EXISTS "public"."tasks_bodyshopid";

View File

@@ -0,0 +1,2 @@
CREATE INDEX "tasks_bodyshopid" on
"public"."tasks" using btree ("bodyshopid");

View File

@@ -80,22 +80,33 @@ const endPoints = InstanceManager({
* @returns {string|string} * @returns {string|string}
*/ */
const formatDate = (date) => { const formatDate = (date) => {
return date ? `| Due on ${moment(date).format("MM/DD/YYYY")}` : ""; return date ? `| Due on: ${moment(date).format("MM/DD/YYYY")}` : "";
};
const formatPriority = (priority) => {
if (priority === 1) {
return "High";
} else if (priority === 3) {
return "Low";
} else {
return "Medium";
}
}; };
/** /**
* Generate the email template arguments. * Generate the email template arguments.
* @param title * @param title
* @param createdBy * @param priority
* @param description
* @param dueDate * @param dueDate
* @param taskId * @param taskId
* @returns {{header, body: string, subHeader: string}} * @returns {{header, body: string, subHeader: string}}
*/ */
const generateTemplateArgs = (title, createdBy, dueDate, taskId) => { const generateTemplateArgs = (title, priority, description, dueDate, taskId) => {
return { return {
header: title, header: title,
subHeader: `Assigned by ${createdBy} ${formatDate(dueDate)}`, subHeader: `Priority: ${formatPriority(priority)} ${formatDate(dueDate)}`,
body: `<a href="${endPoints}/manage/tasks/alltasks?taskid=${taskId}">Please sign in to your account to view the Task details.</a>` body: `${description || ""}<br><a href="${endPoints}/manage/tasks/alltasks?taskid=${taskId}">View this task.</a>`
}; };
}; };
@@ -143,20 +154,29 @@ const sendMail = (type, to, subject, html, taskIds, successCallback) => {
*/ */
const taskAssignedEmail = async (req, res) => { const taskAssignedEmail = async (req, res) => {
// We have no event Data, bail // We have no event Data, bail
if (!req?.payload?.event?.data?.new) { if (!req?.body?.event?.data?.new) {
return res.status(400).json({ message: "No data in the event payload" }); return res.status(400).json({ message: "No data in the event body" });
} }
const { new: newTask } = req.payload.event.data; const { new: newTask } = req.body.event.data;
// This is not a new task, but a reassignment. // This is not a new task, but a reassignment.
const dirty = req.payload.event.data?.old && req.payload.event.data?.old?.assigned_to; const dirty = req.body.event.data?.old && req.body.event.data?.old?.assigned_to;
//Query to get the employee assigned currently.
const {
employees_by_pk: { user_email }
} = await client.request(queries.QUERY_EMPLOYEE_EMAIL_BY_ID, {
id: newTask.assigned_to
});
sendMail( sendMail(
"assigned", "assigned",
newTask.assigned_to, user_email,
`A Task has been ${dirty ? "reassigned" : "created"} for you - ${newTask.title}`, `A ${formatPriority(newTask.priority)} priority task has been ${dirty ? "reassigned to" : "created for"} you - ${newTask.title}`,
generateEmailTemplate(generateTemplateArgs(newTask.title, newTask.created_by, newTask.due_date, newTask.id)) generateEmailTemplate(
generateTemplateArgs(newTask.title, newTask.priority, newTask.description, newTask.due_date, newTask.id)
)
); );
// We return success regardless because we don't want to block the event trigger. // We return success regardless because we don't want to block the event trigger.
@@ -182,7 +202,7 @@ const tasksRemindEmail = async (req, res) => {
// Group tasks by assigned_to, to avoid sending multiple emails to the same recipient. // Group tasks by assigned_to, to avoid sending multiple emails to the same recipient.
const groupedTasks = tasksRequest.tasks.reduce((acc, task) => { const groupedTasks = tasksRequest.tasks.reduce((acc, task) => {
const key = task.assigned_to; const key = task.assigned_to_email.user_email;
if (!acc[key]) { if (!acc[key]) {
acc[key] = []; acc[key] = [];
} }
@@ -217,23 +237,23 @@ const tasksRemindEmail = async (req, res) => {
const onlyTask = groupedTasks[recipient.email][0]; const onlyTask = groupedTasks[recipient.email][0];
emailData.subject = emailData.subject =
`New Task Reminder - ${onlyTask.title} ${onlyTask.due_date ? `- ${formatDate(onlyTask.due_date)}` : ""}`.trim(); `New ${formatPriority(onlyTask.priority)} Priority Task Reminder - ${onlyTask.title} ${onlyTask.due_date ? `- ${formatDate(onlyTask.due_date)}` : ""}`.trim();
emailData.html = generateEmailTemplate( emailData.html = generateEmailTemplate(
generateTemplateArgs(onlyTask.title, onlyTask.created_by, onlyTask.due_date, onlyTask.id) generateTemplateArgs(onlyTask.title, onlyTask.priority, onlyTask.description, onlyTask.due_date, onlyTask.id)
); );
} }
// There are multiple emails to send to this author. // There are multiple emails to send to this author.
else { else {
const allTasks = groupedTasks[recipient.email]; const allTasks = groupedTasks[recipient.email];
emailData.subject = `New Task Reminder - ${allTasks.length} Tasks require your attention`; emailData.subject = `New Tasks Reminder - ${allTasks.length} Tasks require your attention`;
emailData.html = generateEmailTemplate({ emailData.html = generateEmailTemplate({
header: `${allTasks.length} Tasks require your attention`, header: `${allTasks.length} Tasks require your attention`,
subHeader: `Please sign in to your account to view the Task details.`, subHeader: `Please sign in to your account to view the Task details.`,
body: `<ul> body: `<ul>
${allTasks ${allTasks
.map((task) => .map((task) =>
`<li><a href="${endPoints}/manage/tasks/alltasks?taskid=${task.id}">${task.title} ${task.due_date ? `- ${formatDate(task.due_date)}` : ""}</a></li>`.trim() `<li><a href="${endPoints}/manage/tasks/alltasks?taskid=${task.id}">${task.title} - Priority: ${formatPriority(task.priority)} ${task.due_date ? `${formatDate(task.due_date)}` : ""}</a></li>`.trim()
) )
.join("")} .join("")}
</ul>` </ul>`

View File

@@ -2407,9 +2407,14 @@ exports.QUERY_REMIND_TASKS = `
) { ) {
id id
title title
description
due_date due_date
created_by created_by
assigned_to assigned_to
assigned_to_employee {
id
user_email
}
remind_at remind_at
remind_at_sent remind_at_sent
priority priority
@@ -2427,3 +2432,13 @@ exports.UPDATE_TASKS_REMIND_AT_SENT = `mutation UPDATE_TASK_REMIND_AT_SENT($task
affected_rows affected_rows
} }
}`; }`;
exports.QUERY_EMPLOYEE_EMAIL_BY_ID = `
query QUERY_EMPLOYEE_EMAIL_BY_ID($id: uuid!) {
employees_by_pk(id: $id) {
id
user_email
}
}
`;