IO-2278 Parts Dispatching tables & buttons.

This commit is contained in:
Patrick Fic
2023-06-07 08:25:49 -07:00
parent 4dc3bc1532
commit de102d9898
13 changed files with 541 additions and 48 deletions

View File

@@ -1,4 +1,4 @@
<babeledit_project be_version="2.7.1" version="1.2">
<babeledit_project version="1.2" be_version="2.7.1">
<!--
BabelEdit project file
@@ -6622,6 +6622,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>void</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
@@ -35853,6 +35874,141 @@
</folder_node>
</children>
</folder_node>
<folder_node>
<name>parts_dispatch</name>
<children>
<folder_node>
<name>errors</name>
<children>
<concept_node>
<name>creating</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>fields</name>
<children>
<concept_node>
<name>number</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>percent_accepted</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>labels</name>
<children>
<concept_node>
<name>parts_dispatch</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
</children>
</folder_node>
<folder_node>
<name>parts_dispatch_lines</name>
<children>
<folder_node>
<name>fields</name>
<children>
<concept_node>
<name>accepted_at</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
</children>
</folder_node>
<folder_node>
<name>parts_orders</name>
<children>

View File

@@ -107,7 +107,9 @@ export function JobLinesComponent({
onCell: (record) => ({
className: record.manual_line && "job-line-manual",
style: {
...(record.critical ? { boxShadow: " -.5em 0 0 #FFC107" } : {}),
...(record.critical || true
? { boxShadow: " -.5em 0 0 #FFC107" }
: {}),
},
}),
sortOrder:
@@ -122,10 +124,21 @@ export function JobLinesComponent({
sortOrder:
state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
ellipsis: true,
render: (text, record) =>
`${record.oem_partno || ""} ${
record.alt_partno ? `(${record.alt_partno})` : ""
}`.trim(),
onCell: (record) => ({
className: record.manual_line && "job-line-manual",
style: {
...(record.parts_dispatch_lines[0]?.accepted_at || true
? { boxShadow: " -.5em 0 0 #FFC107" }
: {}),
},
}),
render: (text, record) => (
<span class="ant-table-cell-content">
{`${record.oem_partno || ""} ${
record.alt_partno ? `(${record.alt_partno})` : ""
}`.trim()}
</span>
),
},
{
title: t("joblines.fields.op_code_desc"),

View File

@@ -1,18 +1,19 @@
import React, { useState } from "react";
import { useMutation } from "@apollo/client";
import { Button, Form, Popover, Select, Space, notification } from "antd";
import moment from "moment";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_PARTS_DISPATCH } from "../../graphql/parts-dispatch.queries";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { Button, Form, Popover, Select, Space } from "antd";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { useTranslation } from "react-i18next";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import { useMutation } from "@apollo/client";
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
jobRO: selectJobReadOnly,
@@ -42,31 +43,57 @@ export function JobLineDispatchButton({
ro_number: job.ro_number,
});
const { t } = useTranslation();
const [dispatchLines] = useMutation(UPDATE_JOB_LINE);
const [dispatchLines] = useMutation(INSERT_PARTS_DISPATCH);
const handleConvert = async (values) => {
try {
setLoading(true);
//THIS HAS NOT YET BEEN TESTED. START BY FINISHING THIS FUNCTION.
//THIS HAS NOT YET BEEN TESTED. START BY FINISHING THIS FUNCTION.
const result = await dispatchLines({
variables: {
joblinesids: selectedLines.map((l) => l.id),
employeeid: values.employeeid,
note: {
audit: true,
type: "parts",
partsDispatch: {
dispatched_at: moment(),
employeeid: values.employeeid,
jobid: job.id,
created_by: currentUser.email,
text: `${t("joblines.labels.dispatchaudit")}
${selectedLines.map((line) => `line.line_desc \r\n`)}
`,
dispatched_by: currentUser.email,
parts_dispatch_lines: {
data: selectedLines.map((l) => ({
joblineid: l.id,
quantity: l.part_qty,
})),
},
},
//joblineids: selectedLines.map((l) => l.id),
},
});
if (result.errors) {
notification.open({
type: "error",
message: t("parts_dispatch.errors.creating", {
error: JSON.stringify(result.errors),
}),
});
} else {
setSelectedLines([]);
await GenerateDocument(
{
name: Templates.parts_dispatch.key,
variables: {
id: result.data.insert_part_dispatch_one.id,
},
},
{},
"p"
);
}
setVisible(false);
} catch (error) {
console.error(error);
notification.open({
type: "error",
message: t("parts_dispatch.errors.creating", {
error: JSON.stringify(error),
}),
});
} finally {
setLoading(false);
}
@@ -77,7 +104,7 @@ export function JobLineDispatchButton({
<Form layout="vertical" form={form} onFinish={handleConvert}>
<Form.Item
name={"employeeid"}
label={t("jobs.fields.employeeid")}
label={t("timetickets.fields.employee")}
rules={[
{
required: true,
@@ -109,23 +136,6 @@ export function JobLineDispatchButton({
</Select>
</Form.Item>
<Form.Item shouldUpdate>
{() => {
return (
<PrintWrapperComponent
disabled={!form.getFieldValue("employeeid")}
templateObject={{
name: Templates.parts_dispatch.key,
variables: { id: job.id },
}}
messageObject={{
subject: Templates.parts_dispatch.subject,
}}
id={job.id}
/>
);
}}
</Form.Item>
<Space wrap>
<Button type="danger" onClick={() => form.submit()} loading={loading}>
{t("general.actions.save")}

View File

@@ -6,12 +6,14 @@ import BillsListTable from "../bills-list-table/bills-list-table.component";
import JobBillsTotal from "../job-bills-total/job-bills-total.component";
import PartsOrderListTableComponent from "../parts-order-list-table/parts-order-list-table.component";
import PartsOrderModal from "../parts-order-modal/parts-order-modal.container";
import PartsDispatchTable from "../parts-dispatch-table/parts-dispatch-table.component";
export default function JobsDetailPliComponent({
job,
billsQuery,
handleBillOnRowClick,
handlePartsOrderOnRowClick,
handlePartsDispatchOnRowClick,
}) {
return (
<div>
@@ -43,6 +45,13 @@ export default function JobsDetailPliComponent({
billsQuery={billsQuery}
/>
</Col>
<Col span={24}>
<PartsDispatchTable
job={job}
handleOnRowClick={handlePartsDispatchOnRowClick}
billsQuery={billsQuery}
/>
</Col>
</Row>
</div>
);

View File

@@ -39,12 +39,24 @@ export default function JobsDetailPliContainer({ job }) {
}
};
const handlePartsDispatchOnRowClick = (record) => {
if (record) {
if (record.id) {
search.partsdispatchid = record.id;
history.push({ search: queryString.stringify(search) });
}
} else {
delete search.partsdispatchid;
history.push({ search: queryString.stringify(search) });
}
};
return (
<JobsDetailPliComponent
job={job}
billsQuery={billsQuery}
handleBillOnRowClick={handleBillOnRowClick}
handlePartsOrderOnRowClick={handlePartsOrderOnRowClick}
handlePartsDispatchOnRowClick={handlePartsDispatchOnRowClick}
/>
);
}

View File

@@ -0,0 +1,49 @@
import { Card, Col, Row, Table } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { DateTimeFormatter } from "../../utils/DateFormatter";
export default function PartsDispatchExpander({ dispatch, job }) {
const { t } = useTranslation();
const columns = [
{
title: t("joblines.fields.part_qty"),
dataIndex: "quantity",
key: "quantity",
width: "10%",
//sorter: (a, b) => alphaSort(a.number, b.number),
},
{
title: t("joblines.fields.line_desc"),
dataIndex: "joblineid",
key: "joblineid",
//sorter: (a, b) => alphaSort(a.number, b.number),
render: (text, record) => record.jobline.line_desc,
},
{
title: t("parts_dispatch_lines.fields.accepted_at"),
dataIndex: "accepted_at",
key: "accepted_at",
width: "20%",
//sorter: (a, b) => alphaSort(a.number, b.number),
render: (text, record) => (
<DateTimeFormatter>{record.accepted_at}</DateTimeFormatter>
),
},
];
return (
<Card>
<Row gutter={[16, 16]}>
<Col span={24}>
<Table
pagination={false}
dataSource={dispatch.parts_dispatch_lines}
columns={columns}
/>
</Col>
</Row>
</Card>
);
}

View File

@@ -0,0 +1,155 @@
import {
MinusCircleTwoTone,
PlusCircleTwoTone,
SyncOutlined,
} from "@ant-design/icons";
import { Button, Card, Input, Space, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort } from "../../utils/sorters";
import PartsDispatchExpander from "../parts-dispatch-expander/parts-dispatch-expander.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({});
export function PartDispatchTableComponent({
bodyshop,
jobRO,
job,
billsQuery,
handleOnRowClick,
}) {
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
});
// const search = queryString.parse(useLocation().search);
// const selectedBill = search.billid;
const [searchText, setSearchText] = useState("");
const Templates = TemplateList("job_special");
const { refetch } = billsQuery;
const recordActions = (record) => (
<Space wrap>
<PrintWrapperComponent
templateObject={{
name: Templates.parts_dispatch.key,
variables: { id: record.id },
}}
/>
</Space>
);
const columns = [
{
title: t("parts_dispatch.fields.number"),
dataIndex: "number",
key: "number",
sorter: (a, b) => alphaSort(a.number, b.number),
width: "10%",
sortOrder:
state.sortedInfo.columnKey === "number" && state.sortedInfo.order,
},
{
title: t("timetickets.fields.employee"),
dataIndex: "employeeid",
key: "employeeid",
sorter: (a, b) => alphaSort(a.employeeid, b.employeeid),
sortOrder:
state.sortedInfo.columnKey === "employeeid" && state.sortedInfo.order,
render: (text, record) => {
const e = bodyshop.employees.find((e) => e.id === record.employeeid);
return `${e?.first_name || ""} ${e?.last_name || ""}`.trim();
},
},
{
title: t("parts_dispatch.fields.percent_accepted"),
dataIndex: "percent_accepted",
key: "percent_accepted",
render: (text, record) =>
record.parts_dispatch_lines.length > 0
? `
${(
(record.parts_dispatch_lines.filter((l) => l.accepted_at)
.length /
record.parts_dispatch_lines.length) *
100
).toFixed(0)}%`
: "0%",
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
width: "10%",
render: (text, record) => recordActions(record, true),
},
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<Card
title={t("parts_dispatch.labels.parts_dispatch")}
extra={
<Space wrap>
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<Input.Search
placeholder={t("general.labels.search")}
value={searchText}
onChange={(e) => {
e.preventDefault();
setSearchText(e.target.value);
}}
/>
</Space>
}
>
<Table
loading={billsQuery.loading}
scroll={{
x: true, // y: "50rem"
}}
expandable={{
expandedRowRender: (record) => (
<PartsDispatchExpander dispatch={record} job={job} />
),
rowExpandable: (record) => true,
expandIcon: ({ expanded, onExpand, record }) =>
expanded ? (
<MinusCircleTwoTone onClick={(e) => onExpand(record, e)} />
) : (
<PlusCircleTwoTone onClick={(e) => onExpand(record, e)} />
),
}}
columns={columns}
rowKey="id"
dataSource={billsQuery.data ? billsQuery.data.parts_dispatch : []}
onChange={handleTableChange}
/>
</Card>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(PartDispatchTableComponent);

View File

@@ -24,11 +24,7 @@ export const QUERY_ALL_BILLS_PAGINATED = gql`
$limit: Int
$order: [bills_order_by!]!
) {
bills(
offset: $offset
limit: $limit
order_by: $order
) {
bills(offset: $offset, limit: $limit, order_by: $order) {
id
vendorid
vendor {
@@ -97,6 +93,23 @@ export const QUERY_BILLS_BY_JOBID = gql`
comments
user_email
}
parts_dispatch(where: { jobid: { _eq: $jobid } }) {
id
dispatched_at
dispatched_by
employeeid
number
parts_dispatch_lines {
joblineid
id
quantity
accepted_at
jobline {
id
line_desc
}
}
}
bills(where: { jobid: { _eq: $jobid } }, order_by: { date: desc }) {
id
vendorid

View File

@@ -724,6 +724,14 @@ export const GET_JOB_BY_PK = gql`
convertedtolbr
ah_detail_line
critical
parts_dispatch_lines(limit: 1, order_by: { accepted_at: desc }) {
id
accepted_at
parts_dispatch {
id
employeeid
}
}
billlines(limit: 1, order_by: { bill: { date: desc } }) {
id
quantity

View File

@@ -0,0 +1,17 @@
import { gql } from "@apollo/client";
export const INSERT_PARTS_DISPATCH = gql`
mutation INSERT_PARTS_DISPATCH($partsDispatch: parts_dispatch_insert_input!) {
insert_parts_dispatch_one(object: $partsDispatch) {
id
jobid
number
employeeid
parts_dispatch_lines {
id
joblineid
quantity
}
}
}
`;

View File

@@ -2125,6 +2125,23 @@
"orderinhouse": "Order as In House"
}
},
"parts_dispatch": {
"errors": {
"creating": "Error dispatching parts. {{error}}"
},
"fields": {
"number": "Number",
"percent_accepted": "% Accepted"
},
"labels": {
"parts_dispatch": "Parts Dispatch"
}
},
"parts_dispatch_lines": {
"fields": {
"accepted_at": "Accepted At"
}
},
"parts_orders": {
"actions": {
"backordered": "Mark Backordered",

View File

@@ -2125,6 +2125,23 @@
"orderinhouse": ""
}
},
"parts_dispatch": {
"errors": {
"creating": ""
},
"fields": {
"number": "",
"percent_accepted": ""
},
"labels": {
"parts_dispatch": ""
}
},
"parts_dispatch_lines": {
"fields": {
"accepted_at": ""
}
},
"parts_orders": {
"actions": {
"backordered": "",

View File

@@ -2125,6 +2125,23 @@
"orderinhouse": ""
}
},
"parts_dispatch": {
"errors": {
"creating": ""
},
"fields": {
"number": "",
"percent_accepted": ""
},
"labels": {
"parts_dispatch": ""
}
},
"parts_dispatch_lines": {
"fields": {
"accepted_at": ""
}
},
"parts_orders": {
"actions": {
"backordered": "",