371 lines
11 KiB
JavaScript
371 lines
11 KiB
JavaScript
import {
|
|
CheckCircleFilled,
|
|
CheckCircleOutlined,
|
|
DeleteFilled,
|
|
DeleteOutlined,
|
|
EditFilled,
|
|
PlusCircleFilled,
|
|
SyncOutlined
|
|
} from "@ant-design/icons";
|
|
import { Button, Card, Space, Switch } from "antd";
|
|
import queryString from "query-string";
|
|
import { useCallback, useEffect } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { connect } from "react-redux";
|
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
|
import { pageLimit } from "../../utils/config";
|
|
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter.jsx";
|
|
import dayjs from "../../utils/day";
|
|
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
|
|
import PriorityLabel from "../../utils/tasksPriorityLabel.jsx";
|
|
import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
|
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
|
|
|
/**
|
|
* Task List Component
|
|
* @param dueDate
|
|
* @returns {Element}
|
|
* @constructor
|
|
*/
|
|
const DueDateRecord = ({ dueDate }) => {
|
|
if (!dueDate) return <></>;
|
|
|
|
const dueDateDayjs = dayjs(dueDate);
|
|
const relativeDueDate = dueDateDayjs.fromNow();
|
|
const isBeforeToday = dueDateDayjs.isBefore(dayjs());
|
|
|
|
return (
|
|
<div title={relativeDueDate} style={isBeforeToday ? { color: "red" } : {}}>
|
|
<DateFormatter>{dueDate}</DateFormatter>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const RemindAtRecord = ({ remindAt }) => {
|
|
if (!remindAt) return <></>;
|
|
|
|
const remindAtDayjs = dayjs(remindAt);
|
|
const relativeRemindAtDate = remindAtDayjs.fromNow();
|
|
const isBeforeToday = remindAtDayjs.isBefore(dayjs());
|
|
|
|
return (
|
|
<div title={relativeRemindAtDate} style={isBeforeToday ? { color: "red" } : {}}>
|
|
<DateTimeFormatter>{remindAt}</DateTimeFormatter>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const mapDispatchToProps = (dispatch) => ({
|
|
// Existing dispatch props...
|
|
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
|
});
|
|
|
|
const mapStateToProps = () => ({});
|
|
|
|
export default connect(mapStateToProps, mapDispatchToProps)(TaskListComponent);
|
|
|
|
function TaskListComponent({
|
|
bodyshop,
|
|
loading,
|
|
tasks,
|
|
total,
|
|
titleTranslation,
|
|
refetch,
|
|
toggleCompletedStatus,
|
|
setTaskUpsertContext,
|
|
toggleDeletedStatus,
|
|
relationshipType,
|
|
relationshipId,
|
|
onlyMine,
|
|
parentJobId,
|
|
query,
|
|
showRo = true
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const location = useLocation();
|
|
|
|
const search = queryString.parse(useLocation().search);
|
|
|
|
// Extract Query Params
|
|
const { page, sortcolumn, sortorder, deleted, completed, mine } = search;
|
|
|
|
const history = useNavigate();
|
|
const columns = [];
|
|
|
|
useEffect(() => {
|
|
// This is a hack to force the page to change if the query params change (partssublet for example)
|
|
}, [location]);
|
|
|
|
columns.push({
|
|
title: t("tasks.fields.created_at"),
|
|
dataIndex: "created_at",
|
|
key: "created_at",
|
|
width: "10%",
|
|
defaultSortOrder: "descend",
|
|
sorter: true,
|
|
sortOrder: sortcolumn === "created_at" && sortorder,
|
|
render: (text, record) => <DateTimeFormatter>{record.created_at}</DateTimeFormatter>
|
|
});
|
|
|
|
columns.push({
|
|
title: t("tasks.fields.created_by"),
|
|
dataIndex: "created_by",
|
|
key: "created_by",
|
|
width: "8%",
|
|
defaultSortOrder: "descend",
|
|
sorter: true,
|
|
sortOrder: sortcolumn === "created_by" && sortorder,
|
|
render: (text, record) => record.created_by
|
|
});
|
|
|
|
if (!onlyMine) {
|
|
columns.push({
|
|
title: t("tasks.fields.assigned_to"),
|
|
dataIndex: "assigned_to",
|
|
key: "assigned_to",
|
|
width: "8%",
|
|
sorter: true,
|
|
sortOrder: sortcolumn === "assigned_to" && sortorder,
|
|
render: (text, record) => {
|
|
const employee = bodyshop?.employees?.find((e) => e.id === record.assigned_to);
|
|
return employee ? `${employee.first_name} ${employee.last_name}` : t("general.labels.na");
|
|
}
|
|
});
|
|
}
|
|
|
|
columns.push({
|
|
title: t("tasks.fields.related_items"),
|
|
key: "related_items",
|
|
width: "12%",
|
|
render: (text, record) => {
|
|
const items = [];
|
|
|
|
// Job
|
|
if (showRo && record.job) {
|
|
items.push(
|
|
<Link key="job" to={`/manage/jobs/${record.job.id}?tab=tasks`}>
|
|
{t("tasks.fields.job.ro_number")}: {record.job.ro_number}
|
|
</Link>
|
|
);
|
|
}
|
|
if (showRo && !record.job) {
|
|
items.push(`${t("tasks.fields.job.ro_number")}: ${t("general.labels.na")}`);
|
|
}
|
|
|
|
// Jobline
|
|
if (record.jobline?.line_desc) {
|
|
items.push(
|
|
<span key="jobline">
|
|
{t("tasks.fields.jobline")}: {record.jobline.line_desc}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
// Parts Order
|
|
if (record.parts_order) {
|
|
const { order_number, vendor } = record.parts_order;
|
|
const partsOrderText =
|
|
order_number && vendor?.name ? `${order_number} - ${vendor.name}` : t("general.labels.na");
|
|
items.push(
|
|
<Link
|
|
key="parts_order"
|
|
to={`/manage/jobs/${record.job.id}?partsorderid=${record.parts_order.id}&tab=partssublet`}
|
|
>
|
|
{t("tasks.fields.parts_order")}: {partsOrderText}
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
// Bill
|
|
if (record.bill) {
|
|
const { invoice_number, vendor } = record.bill;
|
|
const billText = invoice_number && vendor?.name ? `${invoice_number} - ${vendor.name}` : t("general.labels.na");
|
|
items.push(
|
|
<Link key="bill" to={`/manage/jobs/${record.job.id}?billid=${record.bill.id}&tab=partssublet`}>
|
|
{t("tasks.fields.bill")}: {billText}
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
return items.length > 0 ? <Space orientation="vertical">{items}</Space> : null;
|
|
}
|
|
});
|
|
|
|
columns.push(
|
|
{
|
|
title: t("tasks.fields.title"),
|
|
dataIndex: "title",
|
|
key: "title",
|
|
minWidth: "20%",
|
|
sorter: true,
|
|
sortOrder: sortcolumn === "title" && sortorder
|
|
},
|
|
{
|
|
title: t("tasks.fields.due_date"),
|
|
dataIndex: "due_date",
|
|
key: "due_date",
|
|
sorter: true,
|
|
sortOrder: sortcolumn === "due_date" && sortorder,
|
|
width: "8%",
|
|
render: (text, record) => <DueDateRecord dueDate={record.due_date} />
|
|
},
|
|
{
|
|
title: t("tasks.fields.remind_at"),
|
|
dataIndex: "remind_at",
|
|
key: "remind_at",
|
|
sorter: true,
|
|
sortOrder: sortcolumn === "remind_at" && sortorder,
|
|
width: "10%",
|
|
render: (text, record) => <RemindAtRecord remindAt={record.remind_at} />
|
|
},
|
|
{
|
|
title: t("tasks.fields.priority"),
|
|
dataIndex: "priority",
|
|
key: "priority",
|
|
sorter: true,
|
|
sortOrder: sortcolumn === "priority" && sortorder,
|
|
width: "8%",
|
|
render: (text, record) => <PriorityLabel priority={record.priority} />
|
|
},
|
|
{
|
|
title: t("tasks.fields.actions"),
|
|
key: "toggleCompleted",
|
|
width: 260,
|
|
render: (text, record) => (
|
|
<Space orientation="horizontal" size="small" style={{ maxWidth: "100%", whiteSpace: "nowrap" }}>
|
|
<ShareToTeamsButton
|
|
linkText=""
|
|
urlOverride={`${window.location.origin}/manage/tasks/alltasks?taskid=${record.id}`}
|
|
/>
|
|
<Button
|
|
title={t("tasks.buttons.edit")}
|
|
icon={<EditFilled />}
|
|
onClick={() => {
|
|
setTaskUpsertContext({
|
|
context: {
|
|
existingTask: record,
|
|
query
|
|
}
|
|
});
|
|
}}
|
|
/>
|
|
<Button
|
|
title={t("tasks.buttons.complete")}
|
|
onClick={() => toggleCompletedStatus(record.id, record.completed)}
|
|
icon={record.completed ? <CheckCircleOutlined /> : <CheckCircleFilled />}
|
|
/>
|
|
|
|
<Button
|
|
title={t("tasks.buttons.delete")}
|
|
onClick={() => toggleDeletedStatus(record.id, record.deleted)}
|
|
icon={record.deleted ? <DeleteOutlined /> : <DeleteFilled />}
|
|
/>
|
|
</Space>
|
|
)
|
|
}
|
|
);
|
|
|
|
const handleCreateTask = useCallback(() => {
|
|
setTaskUpsertContext({
|
|
actions: {},
|
|
context: {
|
|
jobid: parentJobId,
|
|
[relationshipType]: relationshipId,
|
|
query
|
|
}
|
|
});
|
|
}, [parentJobId, relationshipId, relationshipType, setTaskUpsertContext, query]);
|
|
|
|
const handleTableChange = (pagination, filters, sorter) => {
|
|
search.page = pagination.current;
|
|
search.sortcolumn = sorter.columnKey;
|
|
search.sortorder = sorter.order;
|
|
history({ search: queryString.stringify(search) });
|
|
};
|
|
|
|
const handleSwitchChange = useCallback(
|
|
(param, value) => {
|
|
if (value) {
|
|
search[param] = "true";
|
|
} else {
|
|
delete search[param];
|
|
}
|
|
logImEXEvent("tasks_filter", { key: param });
|
|
history({ search: queryString.stringify(search) });
|
|
},
|
|
[history, search]
|
|
);
|
|
|
|
const expandableRow = (record) => {
|
|
return (
|
|
<Card title={t("tasks.fields.description")} size="small">
|
|
{record.description}
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Extra actions for the tasks
|
|
* @type {Function}
|
|
*/
|
|
const tasksExtra = useCallback(() => {
|
|
return (
|
|
<Space orientation="horizontal">
|
|
{!onlyMine && (
|
|
<Switch
|
|
checkedChildren={t("tasks.buttons.myTasks")}
|
|
unCheckedChildren={t("tasks.buttons.allTasks")}
|
|
title={t("tasks.titles.mine")}
|
|
checked={mine === "true"}
|
|
onChange={(value) => handleSwitchChange("mine", value)}
|
|
/>
|
|
)}
|
|
<Switch
|
|
checkedChildren={<CheckCircleFilled />}
|
|
unCheckedChildren={<CheckCircleOutlined />}
|
|
title={t("tasks.titles.completed")}
|
|
checked={completed === "true"}
|
|
onChange={(value) => handleSwitchChange("completed", value)}
|
|
/>
|
|
<Switch
|
|
checkedChildren={<DeleteFilled />}
|
|
unCheckedChildren={<DeleteOutlined />}
|
|
title={t("tasks.titles.deleted")}
|
|
checked={deleted === "true"}
|
|
onChange={(value) => handleSwitchChange("deleted", value)}
|
|
/>
|
|
<Button title={t("tasks.buttons.refresh")} onClick={() => refetch()} icon={<SyncOutlined />} />
|
|
|
|
<Button title={t("tasks.buttons.create")} onClick={handleCreateTask} icon={<PlusCircleFilled />}>
|
|
{t("tasks.buttons.create")}
|
|
</Button>
|
|
</Space>
|
|
);
|
|
}, [refetch, deleted, completed, mine, onlyMine, t, handleSwitchChange, handleCreateTask]);
|
|
|
|
return (
|
|
<Card title={titleTranslation} extra={tasksExtra()}>
|
|
<ResponsiveTable
|
|
loading={loading}
|
|
pagination={{
|
|
pageSize: pageLimit,
|
|
current: parseInt(page || 1),
|
|
total: total,
|
|
responsive: true,
|
|
showQuickJumper: true
|
|
}}
|
|
columns={columns}
|
|
mobileColumnKeys={["title", "due_date", "priority", "toggleCompleted"]}
|
|
rowKey="id"
|
|
dataSource={tasks}
|
|
onChange={handleTableChange}
|
|
expandable={{
|
|
expandedRowRender: expandableRow,
|
|
rowExpandable: (record) => record.description
|
|
}}
|
|
/>
|
|
</Card>
|
|
);
|
|
}
|