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 BabelEdit project file
@@ -6622,6 +6622,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> </children>
</folder_node> </folder_node>
<folder_node> <folder_node>
@@ -35853,6 +35874,141 @@
</folder_node> </folder_node>
</children> </children>
</folder_node> </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> <folder_node>
<name>parts_orders</name> <name>parts_orders</name>
<children> <children>

View File

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

View File

@@ -1,18 +1,19 @@
import React, { useState } from "react"; 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 { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { INSERT_PARTS_DISPATCH } from "../../graphql/parts-dispatch.queries";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import { Button, Form, Popover, Select, Space } from "antd"; import { GenerateDocument } from "../../utils/RenderTemplate";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { useTranslation } from "react-i18next";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import { useMutation } from "@apollo/client";
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
jobRO: selectJobReadOnly, jobRO: selectJobReadOnly,
@@ -42,31 +43,57 @@ export function JobLineDispatchButton({
ro_number: job.ro_number, ro_number: job.ro_number,
}); });
const { t } = useTranslation(); const { t } = useTranslation();
const [dispatchLines] = useMutation(UPDATE_JOB_LINE); const [dispatchLines] = useMutation(INSERT_PARTS_DISPATCH);
const handleConvert = async (values) => { const handleConvert = async (values) => {
try { try {
setLoading(true); 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({ const result = await dispatchLines({
variables: { variables: {
joblinesids: selectedLines.map((l) => l.id), partsDispatch: {
employeeid: values.employeeid, dispatched_at: moment(),
note: { employeeid: values.employeeid,
audit: true,
type: "parts",
jobid: job.id, jobid: job.id,
created_by: currentUser.email, dispatched_by: currentUser.email,
text: `${t("joblines.labels.dispatchaudit")} parts_dispatch_lines: {
${selectedLines.map((line) => `line.line_desc \r\n`)} 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); setVisible(false);
} catch (error) { } catch (error) {
console.error(error); notification.open({
type: "error",
message: t("parts_dispatch.errors.creating", {
error: JSON.stringify(error),
}),
});
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -77,7 +104,7 @@ export function JobLineDispatchButton({
<Form layout="vertical" form={form} onFinish={handleConvert}> <Form layout="vertical" form={form} onFinish={handleConvert}>
<Form.Item <Form.Item
name={"employeeid"} name={"employeeid"}
label={t("jobs.fields.employeeid")} label={t("timetickets.fields.employee")}
rules={[ rules={[
{ {
required: true, required: true,
@@ -109,23 +136,6 @@ export function JobLineDispatchButton({
</Select> </Select>
</Form.Item> </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> <Space wrap>
<Button type="danger" onClick={() => form.submit()} loading={loading}> <Button type="danger" onClick={() => form.submit()} loading={loading}>
{t("general.actions.save")} {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 JobBillsTotal from "../job-bills-total/job-bills-total.component";
import PartsOrderListTableComponent from "../parts-order-list-table/parts-order-list-table.component"; import PartsOrderListTableComponent from "../parts-order-list-table/parts-order-list-table.component";
import PartsOrderModal from "../parts-order-modal/parts-order-modal.container"; import PartsOrderModal from "../parts-order-modal/parts-order-modal.container";
import PartsDispatchTable from "../parts-dispatch-table/parts-dispatch-table.component";
export default function JobsDetailPliComponent({ export default function JobsDetailPliComponent({
job, job,
billsQuery, billsQuery,
handleBillOnRowClick, handleBillOnRowClick,
handlePartsOrderOnRowClick, handlePartsOrderOnRowClick,
handlePartsDispatchOnRowClick,
}) { }) {
return ( return (
<div> <div>
@@ -43,6 +45,13 @@ export default function JobsDetailPliComponent({
billsQuery={billsQuery} billsQuery={billsQuery}
/> />
</Col> </Col>
<Col span={24}>
<PartsDispatchTable
job={job}
handleOnRowClick={handlePartsDispatchOnRowClick}
billsQuery={billsQuery}
/>
</Col>
</Row> </Row>
</div> </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 ( return (
<JobsDetailPliComponent <JobsDetailPliComponent
job={job} job={job}
billsQuery={billsQuery} billsQuery={billsQuery}
handleBillOnRowClick={handleBillOnRowClick} handleBillOnRowClick={handleBillOnRowClick}
handlePartsOrderOnRowClick={handlePartsOrderOnRowClick} 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 $limit: Int
$order: [bills_order_by!]! $order: [bills_order_by!]!
) { ) {
bills( bills(offset: $offset, limit: $limit, order_by: $order) {
offset: $offset
limit: $limit
order_by: $order
) {
id id
vendorid vendorid
vendor { vendor {
@@ -97,6 +93,23 @@ export const QUERY_BILLS_BY_JOBID = gql`
comments comments
user_email 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 }) { bills(where: { jobid: { _eq: $jobid } }, order_by: { date: desc }) {
id id
vendorid vendorid

View File

@@ -724,6 +724,14 @@ export const GET_JOB_BY_PK = gql`
convertedtolbr convertedtolbr
ah_detail_line ah_detail_line
critical 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 } }) { billlines(limit: 1, order_by: { bill: { date: desc } }) {
id id
quantity 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" "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": { "parts_orders": {
"actions": { "actions": {
"backordered": "Mark Backordered", "backordered": "Mark Backordered",

View File

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

View File

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