Files
bodyshop/client/src/components/task-list/task-list.component.jsx
Allan Carr 6cac0f9594 IO-3010 Task Table UI refactor
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-10-30 17:38:09 -07:00

399 lines
11 KiB
JavaScript

import {
CheckCircleFilled,
CheckCircleOutlined,
DeleteFilled,
DeleteOutlined,
EditFilled,
ExclamationCircleFilled,
PlusCircleFilled,
SyncOutlined
} from "@ant-design/icons";
import { Button, Card, Space, Switch, Table } from "antd";
import queryString from "query-string";
import React, { 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";
/**
* 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>
);
};
/**
* Priority Label Component
* @param priority
* @returns {Element}
* @constructor
*/
const PriorityLabel = ({ priority }) => {
switch (priority) {
case 1:
return (
<div>
High <ExclamationCircleFilled style={{ marginLeft: "5px", color: "red" }} />
</div>
);
case 2:
return (
<div>
Medium <ExclamationCircleFilled style={{ marginLeft: "5px", color: "yellow" }} />
</div>
);
case 3:
return (
<div>
Low <ExclamationCircleFilled style={{ marginLeft: "5px", color: "green" }} />
</div>
);
default:
return (
<div>
None <ExclamationCircleFilled style={{ marginLeft: "5px" }} />
</div>
);
}
};
const mapDispatchToProps = (dispatch) => ({
// Existing dispatch props...
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
});
const mapStateToProps = (state) => ({});
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 direction="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: "8%",
render: (text, record) => (
<Space direction="horizontal">
<Button
title={t("tasks.buttons.edit")}
onClick={() => {
setTaskUpsertContext({
context: {
existingTask: record,
query
}
});
}}
>
<EditFilled />
</Button>
<Button
title={t("tasks.buttons.complete")}
onClick={() => toggleCompletedStatus(record.id, record.completed)}
>
{record.completed ? <CheckCircleOutlined /> : <CheckCircleFilled />}
</Button>
<Button title={t("tasks.buttons.delete")} onClick={() => toggleDeletedStatus(record.id, record.deleted)}>
{record.deleted ? <DeleteOutlined /> : <DeleteFilled />}
</Button>
</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];
}
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 direction="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()}>
<SyncOutlined />
</Button>
<Button title={t("tasks.buttons.create")} onClick={handleCreateTask}>
<PlusCircleFilled />
{t("tasks.buttons.create")}
</Button>
</Space>
);
}, [refetch, deleted, completed, mine, onlyMine, t, handleSwitchChange, handleCreateTask]);
return (
<Card title={titleTranslation} extra={tasksExtra()}>
<Table
loading={loading}
pagination={{
pageSize: pageLimit,
current: parseInt(page || 1),
total: total,
responsive: true,
showQuickJumper: true
}}
columns={columns}
rowKey="id"
scroll={{ x: true }}
dataSource={tasks}
onChange={handleTableChange}
expandable={{
expandedRowRender: expandableRow,
rowExpandable: (record) => record.description
}}
/>
</Card>
);
}