Compare commits

...

13 Commits

Author SHA1 Message Date
Patrick Fic
26dc720929 Add technician side to part dispatching. 2023-08-04 13:24:41 -07:00
Patrick Fic
b5cc1c06df Merge branch 'release/2023-08-11' into feature/IO-2278-parts-dispatching 2023-08-04 12:36:30 -07:00
Patrick Fic
bf18e687da Schema updates for parts dispatch. 2023-08-04 12:36:08 -07:00
Patrick Fic
786c790307 WIP Tech Claiming Ability 2023-08-04 12:28:08 -07:00
Allan Carr
e69e844568 Merged in release/2023-08-04 (pull request #920)
Release/2023 08 04
2023-08-04 18:06:05 +00:00
Allan Carr
2b5268fb77 Merged in feature/IO-2371-Closing-Period (pull request #918)
IO-2371 Closing Period
2023-08-04 17:35:45 +00:00
Allan Carr
eebe7edba8 IO-2371 Closing period timezone adjustments 2023-08-04 10:36:01 -07:00
Allan Carr
1a5c74dc79 IO-2371 Closing Period 2023-08-04 09:05:03 -07:00
Allan Carr
be62ab5ff9 Merged in feature/IO-2370-Work-In-Progress-Jobs (pull request #916)
IO-2370 Work In Progress Jobs
2023-08-02 23:37:51 +00:00
Patrick Fic
de102d9898 IO-2278 Parts Dispatching tables & buttons. 2023-06-07 08:25:49 -07:00
Patrick Fic
4dc3bc1532 Merge branch 'release/2023-06-09' into feature/IO-2278-parts-dispatching 2023-06-06 13:33:25 -07:00
Patrick Fic
50da8cfbc6 Merge branch 'release/2023-06-09' into feature/IO-2278-parts-dispatching 2023-06-06 13:20:10 -07:00
Patrick Fic
68f4237e15 IO-2278 Begin parts dispatching including basic items and schema. 2023-06-01 13:27:08 -07:00
31 changed files with 1403 additions and 53 deletions

View File

@@ -1474,6 +1474,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>admin_jobuninvoice</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> <concept_node>
<name>admin_jobunvoid</name> <name>admin_jobunvoid</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -3571,6 +3592,27 @@
<folder_node> <folder_node>
<name>validation</name> <name>validation</name>
<children> <children>
<concept_node>
<name>closingperiod</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> <concept_node>
<name>inventoryquantity</name> <name>inventoryquantity</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -4205,6 +4247,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>closingperiod</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> <concept_node>
<name>country</name> <name>country</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -19461,6 +19524,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>dispatchparts</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> <concept_node>
<name>new</name> <name>new</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -23689,6 +23773,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>date_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>
<concept_node> <concept_node>
<name>ded_amt</name> <name>ded_amt</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -28469,6 +28574,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>closingperiod</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> <concept_node>
<name>contracts</name> <name>contracts</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -33870,6 +33996,27 @@
<folder_node> <folder_node>
<name>tech</name> <name>tech</name>
<children> <children>
<concept_node>
<name>dispatchedparts</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> <concept_node>
<name>home</name> <name>home</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -35958,6 +36105,188 @@
</folder_node> </folder_node>
</children> </children>
</folder_node> </folder_node>
<folder_node>
<name>parts_dispatch</name>
<children>
<folder_node>
<name>actions</name>
<children>
<concept_node>
<name>accept</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>errors</name>
<children>
<concept_node>
<name>accepting</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>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>
@@ -40685,6 +41014,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>parts_return_slip</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> <concept_node>
<name>sublet_order</name> <name>sublet_order</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -44536,6 +44886,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>work_in_progress_jobs</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> <concept_node>
<name>work_in_progress_labour</name> <name>work_in_progress_labour</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>

View File

@@ -1,6 +1,6 @@
import Icon, { UploadOutlined } from "@ant-design/icons"; import Icon, { UploadOutlined } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client"; import { useApolloClient } from "@apollo/client";
import { MdOpenInNew } from "react-icons/md"; import { useTreatments } from "@splitsoftware/splitio-react";
import { import {
Alert, Alert,
Divider, Divider,
@@ -12,14 +12,17 @@ import {
Switch, Switch,
Upload, Upload,
} from "antd"; } from "antd";
import moment from "moment";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { MdOpenInNew } from "react-icons/md";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries"; import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component"; import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
@@ -28,8 +31,6 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component"; import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import BillFormLines from "./bill-form.lines.component"; import BillFormLines from "./bill-form.lines.component";
import { CalculateBillTotal } from "./bill-form.totals.utility"; import { CalculateBillTotal } from "./bill-form.totals.utility";
import { useTreatments } from "@splitsoftware/splitio-react";
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -58,6 +59,11 @@ export function BillFormComponent({
{}, {},
bodyshop.imexshopid bodyshop.imexshopid
); );
const { ClosingPeriod } = useTreatments(
["ClosingPeriod"],
{},
bodyshop.imexshopid
);
const handleVendorSelect = (props, opt) => { const handleVendorSelect = (props, opt) => {
setDiscount(opt.discount); setDiscount(opt.discount);
@@ -259,6 +265,37 @@ export function BillFormComponent({
required: true, required: true,
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, },
({ getFieldValue }) => ({
validator(rule, value) {
if (
ClosingPeriod.treatment === "on" &&
bodyshop.accountingconfig.ClosingPeriod
) {
if (
moment(value)
.startOf("day")
.isSameOrAfter(
moment(
bodyshop.accountingconfig.ClosingPeriod[0]
).startOf("day")
) &&
moment(value)
.startOf("day")
.isSameOrBefore(
moment(
bodyshop.accountingconfig.ClosingPeriod[1]
).endOf("day")
)
) {
return Promise.resolve();
} else {
return Promise.reject(t("bills.validation.closingperiod"));
}
} else {
return Promise.resolve();
}
},
}),
]} ]}
> >
<FormDatePicker disabled={disabled} /> <FormDatePicker disabled={disabled} />

View File

@@ -8,7 +8,18 @@ import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
export default function JobLinesExpander({ jobline, jobid }) { import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(JobLinesExpander);
export function JobLinesExpander({ jobline, jobid, bodyshop }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { loading, error, data } = useQuery(GET_JOB_LINE_ORDERS, { const { loading, error, data } = useQuery(GET_JOB_LINE_ORDERS, {
fetchPolicy: "network-only", fetchPolicy: "network-only",
@@ -23,7 +34,7 @@ export default function JobLinesExpander({ jobline, jobid }) {
return ( return (
<Row> <Row>
<Col md={24} lg={12}> <Col md={24} lg={8}>
<Typography.Title level={4}> <Typography.Title level={4}>
{t("parts_orders.labels.parts_orders")} {t("parts_orders.labels.parts_orders")}
</Typography.Title> </Typography.Title>
@@ -49,7 +60,7 @@ export default function JobLinesExpander({ jobline, jobid }) {
)} )}
</Timeline> </Timeline>
</Col> </Col>
<Col md={24} lg={12}> <Col md={24} lg={8}>
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title> <Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
<Timeline> <Timeline>
{data.billlines.length > 0 ? ( {data.billlines.length > 0 ? (
@@ -71,7 +82,7 @@ export default function JobLinesExpander({ jobline, jobid }) {
</Col> </Col>
<Col span={4}> <Col span={4}>
<span> <span>
{`${t("billlines.fields.actual_cost")}: `} {`${t("billlines.fields.actual_cost")}: `}
<CurrencyFormatter>{line.actual_cost}</CurrencyFormatter> <CurrencyFormatter>{line.actual_cost}</CurrencyFormatter>
</span> </span>
</Col> </Col>
@@ -89,6 +100,37 @@ export default function JobLinesExpander({ jobline, jobid }) {
)} )}
</Timeline> </Timeline>
</Col> </Col>
<Col md={24} lg={8}>
<Typography.Title level={4}>
{t("parts_dispatch.labels.parts_dispatch")}
</Typography.Title>
<Timeline>
{data.parts_dispatch_lines.length > 0 ? (
data.parts_dispatch_lines.map((line) => (
<Timeline.Item key={line.id}>
<Space split={<Divider type="vertical" />} wrap>
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.id}`}>
{line.parts_dispatch.number}
</Link>
{
bodyshop.employees.find(
(e) => e.id === line.parts_dispatch.employeeid
)?.first_name
}
<Space>
{t("parts_dispatch_lines.fields.accepted_at")}
<DateFormatter>{line.accepted_at}</DateFormatter>
</Space>
</Space>
</Timeline.Item>
))
) : (
<Timeline.Item>
{t("parts_orders.labels.notyetordered")}
</Timeline.Item>
)}
</Timeline>
</Col>
</Row> </Row>
); );
} }

View File

@@ -45,6 +45,7 @@ import JobLinesExpander from "./job-lines-expander.component";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import moment from "moment"; import moment from "moment";
import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component"; import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component";
import JobLineDispatchButton from "../job-line-dispatch-button/job-line-dispatch-button.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -106,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:
@@ -121,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"),
@@ -440,6 +454,11 @@ export function JobLinesComponent({
</Space> </Space>
</Tag> </Tag>
)} )}
<JobLineDispatchButton
selectedLines={selectedLines}
setSelectedLines={setSelectedLines}
job={job}
/>
<Button <Button
disabled={ disabled={
(job && !job.converted) || (job && !job.converted) ||
@@ -448,15 +467,6 @@ export function JobLinesComponent({
technician technician
} }
onClick={() => { onClick={() => {
// setPartsOrderContext({
// actions: { refetch: refetch },
// context: {
// jobId: job.id,
// job: job,
// linesToOrder: selectedLines,
// },
// });
setBillEnterContext({ setBillEnterContext({
actions: { refetch: refetch }, actions: { refetch: refetch },
context: { context: {

View File

@@ -0,0 +1,167 @@
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 { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
jobRO: selectJobReadOnly,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobLineDispatchButton);
export function JobLineDispatchButton({
setSelectedLines,
selectedLines,
bodyshop,
jobRO,
job,
currentUser,
}) {
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
const Templates = TemplateList("job_special", {
ro_number: job.ro_number,
});
const { t } = useTranslation();
const [dispatchLines] = useMutation(INSERT_PARTS_DISPATCH);
const handleConvert = async (values) => {
try {
setLoading(true);
//THIS HAS NOT YET BEEN TESTED. START BY FINISHING THIS FUNCTION.
const result = await dispatchLines({
variables: {
partsDispatch: {
dispatched_at: moment(),
employeeid: values.employeeid,
jobid: job.id,
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: result.errors,
}),
});
} else {
setSelectedLines([]);
await GenerateDocument(
{
name: Templates.parts_dispatch.key,
variables: {
id: result.data.insert_parts_dispatch_one.id,
},
},
{},
"p"
);
}
setVisible(false);
} catch (error) {
notification.open({
type: "error",
message: t("parts_dispatch.errors.creating", {
error: error,
}),
});
} finally {
setLoading(false);
}
};
const popMenu = (
<div>
<Form layout="vertical" form={form} onFinish={handleConvert}>
<Form.Item
name={"employeeid"}
label={t("timetickets.fields.employee")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
showSearch
style={{ width: 200 }}
optionFilterProp="children"
filterOption={(input, option) =>
option.props.children
.toLowerCase()
.indexOf(input.toLowerCase()) >= 0
}
>
{bodyshop.employees
.filter((emp) => emp.active)
.map((emp) => (
<Select.Option
value={emp.id}
key={emp.id}
name={`${emp.first_name} ${emp.last_name}`}
>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
</Select>
</Form.Item>
<Space wrap>
<Button
type="danger"
onClick={() => form.submit()}
loading={loading}
disabled={selectedLines.length === 0}
>
{t("general.actions.save")}
</Button>
<Button onClick={() => setVisible(false)}>
{t("general.actions.cancel")}
</Button>
</Space>
</Form>
</div>
);
return (
<Popover open={visible} content={popMenu}>
<Button
disabled={selectedLines.length === 0 || jobRO}
loading={loading}
onClick={() => setVisible(true)}
>
{t("joblines.actions.dispatchparts", { count: selectedLines.length })}
</Button>
</Popover>
);
}

View File

@@ -1,4 +1,4 @@
import { notification, Select } from "antd"; import { notification, Select, Space } from "antd";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -75,7 +75,10 @@ export function JobLineLocationPopup({ bodyshop, jobline, disabled }) {
style={{ width: "100%", minHeight: "2rem", cursor: "pointer" }} style={{ width: "100%", minHeight: "2rem", cursor: "pointer" }}
onClick={() => !disabled && setEditing(true)} onClick={() => !disabled && setEditing(true)}
> >
{jobline.location} <Space wrap>
{jobline.location}
{jobline.parts_dispatch_lines.length > 0 && "-Disp"}
</Space>
</div> </div>
); );
} }

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,81 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Col, Row, Table, notification } from "antd";
import moment from "moment-business-days";
import React from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_PARTS_DISPATCH_LINE } from "../../graphql/parts-dispatch.queries";
import { DateTimeFormatter } from "../../utils/DateFormatter";
export default function PartsDispatchExpander({ dispatch, job }) {
const { t } = useTranslation();
const [updateDispatchLine] = useMutation(UPDATE_PARTS_DISPATCH_LINE);
const handleAccept = async ({ partsDispatchLineId }) => {
const accepted_at = moment();
const result = await updateDispatchLine({
variables: { id: partsDispatchLineId, line: { accepted_at } },
optimisticResponse: {
update_parts_dispatch_lines_by_pk: {
accepted_at,
id: partsDispatchLineId,
},
},
});
if (result.errors) {
notification.open({
type: "error",
message: t("parts_dispatch.errors.accepting", {
error: JSON.stringify(result.errors),
}),
});
}
};
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) =>
record.accepted_at ? (
<DateTimeFormatter>{record.accepted_at}</DateTimeFormatter>
) : (
<Button
onClick={() => handleAccept({ partsDispatchLineId: record.id })}
>
{t("parts_dispatch.actions.accept")}
</Button>
),
},
];
return (
<Card>
<Row gutter={[16, 16]}>
<Col span={24}>
<Table
rowKey={"id"}
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

@@ -9,9 +9,11 @@ export default function PrintWrapperComponent({
children, children,
id, id,
emailOnly = false, emailOnly = false,
disabled,
}) { }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const handlePrint = async (type) => { const handlePrint = async (type) => {
if (disabled) return;
setLoading(true); setLoading(true);
await GenerateDocument(templateObject, messageObject, type, id); await GenerateDocument(templateObject, messageObject, type, id);
setLoading(false); setLoading(false);
@@ -20,8 +22,18 @@ export default function PrintWrapperComponent({
return ( return (
<Space> <Space>
{children || null} {children || null}
{!emailOnly && <PrinterFilled onClick={() => handlePrint("p")} />} {!emailOnly && (
<MailFilled onClick={() => handlePrint("e")} /> <PrinterFilled
disabled={disabled}
onClick={() => handlePrint("p")}
style={{ cursor: disabled ? "not-allowed" : null }}
/>
)}
<MailFilled
disabled={disabled}
onClick={() => handlePrint("e")}
style={{ cursor: disabled ? "not-allowed" : null }}
/>
{loading && <Spin />} {loading && <Spin />}
</Space> </Space>
); );

View File

@@ -1,14 +1,14 @@
import React, { useEffect, useState } from "react"; import { useMutation, useQuery } from "@apollo/client";
import ShopInfoComponent from "./shop-info.component";
import { Form, notification } from "antd"; import { Form, notification } from "antd";
import { useQuery, useMutation } from "@apollo/client"; import moment from "moment";
import { QUERY_BODYSHOP, UPDATE_SHOP } from "../../graphql/bodyshop.queries"; import React, { useEffect, useState } from "react";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import AlertComponent from "../alert/alert.component";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { QUERY_BODYSHOP, UPDATE_SHOP } from "../../graphql/bodyshop.queries";
import AlertComponent from "../alert/alert.component";
import FormsFieldChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; import FormsFieldChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import moment from "moment"; import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import ShopInfoComponent from "./shop-info.component";
export default function ShopInfoContainer() { export default function ShopInfoContainer() {
const [form] = Form.useForm(); const [form] = Form.useForm();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -52,13 +52,28 @@ export default function ShopInfoContainer() {
onFinish={handleFinish} onFinish={handleFinish}
initialValues={ initialValues={
data data
? { ? data.bodyshops[0].accountingconfig.ClosingPeriod
...data.bodyshops[0], ? {
schedule_start_time: moment( ...data.bodyshops[0],
data.bodyshops[0].schedule_start_time accountingconfig: {
), ...data.bodyshops[0].accountingconfig,
schedule_end_time: moment(data.bodyshops[0].schedule_end_time), ClosingPeriod: [
} moment(data.bodyshops[0].accountingconfig.ClosingPeriod[0]),
moment(data.bodyshops[0].accountingconfig.ClosingPeriod[1]),
],
},
schedule_start_time: moment(
data.bodyshops[0].schedule_start_time
),
schedule_end_time: moment(data.bodyshops[0].schedule_end_time),
}
: {
...data.bodyshops[0],
schedule_start_time: moment(
data.bodyshops[0].schedule_start_time
),
schedule_end_time: moment(data.bodyshops[0].schedule_end_time),
}
: null : null
} }
> >

View File

@@ -1,6 +1,8 @@
import { DeleteFilled } from "@ant-design/icons"; import { DeleteFilled } from "@ant-design/icons";
import { useTreatments } from "@splitsoftware/splitio-react";
import { import {
Button, Button,
DatePicker,
Form, Form,
Input, Input,
InputNumber, InputNumber,
@@ -9,8 +11,13 @@ import {
Space, Space,
Switch, Switch,
} from "antd"; } from "antd";
import momentTZ from "moment-timezone";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import DatePickerRanges from "../../utils/DatePickerRanges";
import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component"; import FormItemEmail from "../form-items-formatted/email-form-item.component";
import PhoneFormItem, { import PhoneFormItem, {
@@ -18,12 +25,22 @@ import PhoneFormItem, {
} from "../form-items-formatted/phone-form-item.component"; } from "../form-items-formatted/phone-form-item.component";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import momentTZ from "moment-timezone";
const timeZonesList = momentTZ.tz.names(); const timeZonesList = momentTZ.tz.names();
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoGeneral);
export default function ShopInfoGeneral({ form }) { export function ShopInfoGeneral({ form, bodyshop }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { ClosingPeriod } = useTreatments(
["ClosingPeriod"],
{},
bodyshop && bodyshop.imexshopid
);
return ( return (
<div> <div>
@@ -392,6 +409,20 @@ export default function ShopInfoGeneral({ form }) {
> >
<Select mode="tags" /> <Select mode="tags" />
</Form.Item> </Form.Item>
{ClosingPeriod.treatment === "on" && (
<>
<Form.Item
allowClear
name={["accountingconfig", "ClosingPeriod"]}
label={t("bodyshop.fields.closingperiod")} //{t("reportcenter.labels.dates")}
>
<DatePicker.RangePicker
format="MM/DD/YYYY"
ranges={DatePickerRanges}
/>
</Form.Item>
</>
)}
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow <LayoutFormRow
header={t("bodyshop.labels.scoreboardsetup")} header={t("bodyshop.labels.scoreboardsetup")}
@@ -604,7 +635,9 @@ export default function ShopInfoGeneral({ form }) {
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name={["md_email_cc", "parts_return_slip"]} name={["md_email_cc", "parts_return_slip"]}
label={t("bodyshop.fields.md_email_cc", { template: "parts_return_slip" })} label={t("bodyshop.fields.md_email_cc", {
template: "parts_return_slip",
})}
rules={[ rules={[
{ {
//message: t("general.validation.required"), //message: t("general.validation.required"),

View File

@@ -1,4 +1,8 @@
import Icon, { SearchOutlined, ScheduleOutlined } from "@ant-design/icons"; import Icon, {
SearchOutlined,
ScheduleOutlined,
CarOutlined,
} from "@ant-design/icons";
import { Layout, Menu } from "antd"; import { Layout, Menu } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -68,6 +72,15 @@ export function TechSider({ technician, techLogout }) {
<Menu.Item key="5" disabled={!!!technician} icon={<ScheduleOutlined />}> <Menu.Item key="5" disabled={!!!technician} icon={<ScheduleOutlined />}>
<Link to={`/tech/list`}>{t("menus.tech.productionlist")}</Link> <Link to={`/tech/list`}>{t("menus.tech.productionlist")}</Link>
</Menu.Item> </Menu.Item>
<Menu.Item
key="dispatchedparts"
disabled={!!!technician}
icon={<CarOutlined />}
>
<Link to={`/tech/dispatchedparts`}>
{t("menus.tech.dispatchedparts")}
</Link>
</Menu.Item>
<Menu.Item <Menu.Item
key="6" key="6"
disabled={!!!technician} disabled={!!!technician}

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

@@ -725,6 +725,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
@@ -2141,6 +2149,18 @@ export const GET_JOB_LINE_ORDERS = gql`
} }
} }
} }
parts_dispatch_lines(where: { joblineid: { _eq: $joblineid } }) {
id
accepted_at
quantity
parts_dispatch {
id
employeeid
dispatched_at
dispatched_by
number
}
}
parts_order_lines(where: { job_line_id: { _eq: $joblineid } }) { parts_order_lines(where: { job_line_id: { _eq: $joblineid } }) {
id id
act_price act_price

View File

@@ -0,0 +1,79 @@
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
}
}
}
`;
export const GET_UNACCEPTED_PARTS_DISPATCH = gql`
query GET_UNACCEPTED_PARTS_DISPATCH(
$techId: uuid!
$offset: Int
$limit: Int
) {
parts_dispatch_aggregate(
where: {
employeeid: { _eq: $techId }
parts_dispatch_lines: { accepted_at: { _is_null: true } }
}
) {
aggregate {
count(distinct: true)
}
}
parts_dispatch(
offset: $offset
limit: $limit
where: {
employeeid: { _eq: $techId }
parts_dispatch_lines: { accepted_at: { _is_null: true } }
}
) {
id
job {
id
ro_number
status
v_make_desc
v_model_desc
v_model_yr
v_color
}
dispatched_at
dispatched_by
parts_dispatch_lines {
id
accepted_at
jobline {
line_desc
id
}
quantity
joblineid
}
}
}
`;
export const UPDATE_PARTS_DISPATCH_LINE = gql`
mutation UPDATE_PARTS_DISPATCH_LINE(
$id: uuid!
$line: parts_dispatch_lines_set_input!
) {
update_parts_dispatch_lines_by_pk(pk_columns: { id: $id }, _set: $line) {
accepted_at
id
}
}
`;

View File

@@ -8,7 +8,6 @@ import {
Form, Form,
Input, Input,
InputNumber, InputNumber,
notification,
PageHeader, PageHeader,
Popconfirm, Popconfirm,
Row, Row,
@@ -17,12 +16,14 @@ import {
Statistic, Statistic,
Switch, Switch,
Typography, Typography,
notification,
} from "antd"; } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
//import { useHistory } from "react-router-dom"; //import { useHistory } from "react-router-dom";
import { useTreatments } from "@splitsoftware/splitio-react"; import { useTreatments } from "@splitsoftware/splitio-react";
import Dinero from "dinero.js";
import moment from "moment"; import moment from "moment";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
@@ -37,7 +38,6 @@ import { generateJobLinesUpdatesForInvoicing } from "../../graphql/jobs-lines.qu
import { UPDATE_JOB } from "../../graphql/jobs.queries"; import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import Dinero from "dinero.js";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
jobRO: selectJobReadOnly, jobRO: selectJobReadOnly,
@@ -55,6 +55,11 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
{}, {},
bodyshop && bodyshop.imexshopid bodyshop && bodyshop.imexshopid
); );
const { ClosingPeriod } = useTreatments(
["ClosingPeriod"],
{},
bodyshop && bodyshop.imexshopid
);
const handleFinish = async ({ removefromproduction, ...values }) => { const handleFinish = async ({ removefromproduction, ...values }) => {
setLoading(true); setLoading(true);
@@ -254,12 +259,40 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
if (!value || moment(value).isSameOrAfter(moment(), "day")) { if (!value || moment(value).isSameOrAfter(moment(), "day")) {
return Promise.resolve(); return Promise.resolve();
} }
return Promise.reject( return Promise.reject(
new Error(t("jobs.labels.dms.invoicedatefuture")) new Error(t("jobs.labels.dms.invoicedatefuture"))
); );
}, },
}), }),
({ getFieldValue }) => ({
validator(_, value) {
if (
ClosingPeriod.treatment === "on" &&
bodyshop.accountingconfig.ClosingPeriod
) {
if (
moment(value).isSameOrAfter(
moment(
bodyshop.accountingconfig.ClosingPeriod[0]
).startOf("day")
) &&
moment(value).isSameOrBefore(
moment(
bodyshop.accountingconfig.ClosingPeriod[1]
).endOf("day")
)
) {
return Promise.resolve();
} else {
return Promise.reject(
new Error(t("jobs.labels.closingperiod"))
);
}
} else {
return Promise.resolve();
}
},
}),
]} ]}
> >
<DateTimePicker <DateTimePicker

View File

@@ -0,0 +1,139 @@
import {
MinusCircleTwoTone,
PlusCircleTwoTone,
SyncOutlined,
} from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { Button, Card, Space, Table } from "antd";
import queryString from "query-string";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import AlertComponent from "../../components/alert/alert.component";
import PartsDispatchExpander from "../../components/parts-dispatch-expander/parts-dispatch-expander.component";
import { GET_UNACCEPTED_PARTS_DISPATCH } from "../../graphql/parts-dispatch.queries";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { alphaSort } from "../../utils/sorters";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
technician: selectTechnician,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({});
export function TechDispatchedParts({ technician, bodyshop }) {
const searchParams = queryString.parse(useLocation().search);
const { page } = searchParams;
const { loading, error, data, refetch } = useQuery(
GET_UNACCEPTED_PARTS_DISPATCH,
{
variables: {
techId: technician.id,
offset: page ? (page - 1) * 25 : 0,
limit: 25,
},
}
);
const { t } = useTranslation();
const history = useHistory();
if (error) return <AlertComponent message={error.message} type="error" />;
const parts_dispatch = data?.parts_dispatch;
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "job.ro_number",
key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
render: (text, record) => record.job.ro_number || t("general.labels.na"),
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => alphaSort(a.status, b.status),
render: (text, record) => {
return record.job.status || t("general.labels.na");
},
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
render: (text, record) => (
<span>{`${record.job.v_model_yr || ""} ${
record.job.v_make_desc || ""
} ${record.job.v_model_desc || ""}`}</span>
),
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<Button onClick={() => {}}>
{t("timetickets.actions.claimtasks")}
</Button>
),
},
];
const handleTableChange = (pagination, filters, sorter) => {
searchParams.page = pagination.current;
history.push({ search: queryString.stringify(searchParams) });
};
return (
<Card
extra={
<Space wrap>
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
</Space>
}
>
<Table
loading={loading}
pagination={{
pageSize: 25,
current: parseInt(page || 1),
total: data ? data.parts_dispatch_aggregate.aggregate.count : 0,
showSizeChanger: false,
}}
columns={columns}
rowKey="id"
dataSource={parts_dispatch}
scroll={{ x: true }}
onChange={handleTableChange}
expandable={{
expandedRowRender: (record) => (
<PartsDispatchExpander dispatch={record} />
),
rowExpandable: (record) => true,
//expandRowByClick: true,
expandIcon: ({ expanded, onExpand, record }) =>
expanded ? (
<MinusCircleTwoTone onClick={(e) => onExpand(record, e)} />
) : (
<PlusCircleTwoTone onClick={(e) => onExpand(record, e)} />
),
}}
/>
</Card>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(TechDispatchedParts);

View File

@@ -35,6 +35,9 @@ const TechJobClock = lazy(() =>
const TechShiftClock = lazy(() => const TechShiftClock = lazy(() =>
import("../tech-shift-clock/tech-shift-clock.component") import("../tech-shift-clock/tech-shift-clock.component")
); );
const TechDispatchedParts = lazy(() =>
import("../tech-dispatched-parts/tech-dispatched-parts.page")
);
const { Content } = Layout; const { Content } = Layout;
@@ -101,6 +104,11 @@ export function TechPage({ technician, match }) {
path={`${match.path}/board`} path={`${match.path}/board`}
component={ProductionBoardPage} component={ProductionBoardPage}
/> />
<Route
exact
path={`${match.path}/dispatchedparts`}
component={TechDispatchedParts}
/>
</Switch> </Switch>
</FeatureWrapper> </FeatureWrapper>
</Suspense> </Suspense>

View File

@@ -223,6 +223,7 @@
"reexport": "Bill marked for re-export." "reexport": "Bill marked for re-export."
}, },
"validation": { "validation": {
"closingperiod": "This Bill Date is outside of the Closing Period.",
"inventoryquantity": "Quantity must be greater than or equal to what has been added to inventory ({{number}}).", "inventoryquantity": "Quantity must be greater than or equal to what has been added to inventory ({{number}}).",
"manualinhouse": "Manual posting to the in house vendor is restricted. ", "manualinhouse": "Manual posting to the in house vendor is restricted. ",
"unique_invoice_number": "This invoice number has already been entered for this vendor." "unique_invoice_number": "This invoice number has already been entered for this vendor."
@@ -262,6 +263,7 @@
"bill_local_tax_rate": "Bill - Provincial/State Tax Rate %", "bill_local_tax_rate": "Bill - Provincial/State Tax Rate %",
"bill_state_tax_rate": "Bill - Provincial/State Tax Rate %", "bill_state_tax_rate": "Bill - Provincial/State Tax Rate %",
"city": "City", "city": "City",
"closingperiod": "Closing Period",
"country": "Country", "country": "Country",
"dailybodytarget": "Scoreboard - Daily Body Target", "dailybodytarget": "Scoreboard - Daily Body Target",
"dailypainttarget": "Scoreboard - Daily Paint Target", "dailypainttarget": "Scoreboard - Daily Paint Target",
@@ -1214,6 +1216,7 @@
"joblines": { "joblines": {
"actions": { "actions": {
"converttolabor": "Convert amount to Labor.", "converttolabor": "Convert amount to Labor.",
"dispatchparts": "Dispatch Parts ({{count}})",
"new": "New Line" "new": "New Line"
}, },
"errors": { "errors": {
@@ -1688,6 +1691,7 @@
"checklists": "Checklists", "checklists": "Checklists",
"closeconfirm": "Are you sure you want to close this job? This cannot be easily undone.", "closeconfirm": "Are you sure you want to close this job? This cannot be easily undone.",
"closejob": "Close Job {{ro_number}}", "closejob": "Close Job {{ro_number}}",
"closingperiod": "This Invoice Date is outside of the Closing Period.",
"contracts": "CC Contracts", "contracts": "CC Contracts",
"convertedtolabor": "Lines Converted to Labor", "convertedtolabor": "Lines Converted to Labor",
"cost": "Cost", "cost": "Cost",
@@ -1991,6 +1995,7 @@
"shops": "My Shops" "shops": "My Shops"
}, },
"tech": { "tech": {
"dispatchedparts": "Dispatched Parts",
"home": "Home", "home": "Home",
"jobclockin": "Job Clock In", "jobclockin": "Job Clock In",
"jobclockout": "Job Clock Out", "jobclockout": "Job Clock Out",
@@ -2131,6 +2136,27 @@
"orderinhouse": "Order as In House" "orderinhouse": "Order as In House"
} }
}, },
"parts_dispatch": {
"actions": {
"accept": "Accept"
},
"errors": {
"accepting": "Error accepting parts dispatch. {{error}}",
"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",
@@ -2408,7 +2434,7 @@
"jobs": { "jobs": {
"individual_job_note": "Job Note RO: {{ro_number}}", "individual_job_note": "Job Note RO: {{ro_number}}",
"parts_order": "Parts Order PO: {{ro_number}} - {{name}}", "parts_order": "Parts Order PO: {{ro_number}} - {{name}}",
"parts_return_slip":"Parts Return PO: {{ro_number}} - {{name}}", "parts_return_slip": "Parts Return PO: {{ro_number}} - {{name}}",
"sublet_order": "Sublet Order PO: {{ro_number}} - {{name}}" "sublet_order": "Sublet Order PO: {{ro_number}} - {{name}}"
} }
}, },

View File

@@ -223,6 +223,7 @@
"reexport": "" "reexport": ""
}, },
"validation": { "validation": {
"closingperiod": "",
"inventoryquantity": "", "inventoryquantity": "",
"manualinhouse": "", "manualinhouse": "",
"unique_invoice_number": "" "unique_invoice_number": ""
@@ -262,6 +263,7 @@
"bill_local_tax_rate": "", "bill_local_tax_rate": "",
"bill_state_tax_rate": "", "bill_state_tax_rate": "",
"city": "", "city": "",
"closingperiod": "",
"country": "", "country": "",
"dailybodytarget": "", "dailybodytarget": "",
"dailypainttarget": "", "dailypainttarget": "",
@@ -1214,6 +1216,7 @@
"joblines": { "joblines": {
"actions": { "actions": {
"converttolabor": "", "converttolabor": "",
"dispatchparts": "",
"new": "" "new": ""
}, },
"errors": { "errors": {
@@ -1688,6 +1691,7 @@
"checklists": "", "checklists": "",
"closeconfirm": "", "closeconfirm": "",
"closejob": "", "closejob": "",
"closingperiod": "",
"contracts": "", "contracts": "",
"convertedtolabor": "", "convertedtolabor": "",
"cost": "", "cost": "",
@@ -1991,6 +1995,7 @@
"shops": "Mis tiendas" "shops": "Mis tiendas"
}, },
"tech": { "tech": {
"dispatchedparts": "",
"home": "", "home": "",
"jobclockin": "", "jobclockin": "",
"jobclockout": "", "jobclockout": "",
@@ -2131,6 +2136,27 @@
"orderinhouse": "" "orderinhouse": ""
} }
}, },
"parts_dispatch": {
"actions": {
"accept": ""
},
"errors": {
"accepting": "",
"creating": ""
},
"fields": {
"number": "",
"percent_accepted": ""
},
"labels": {
"parts_dispatch": ""
}
},
"parts_dispatch_lines": {
"fields": {
"accepted_at": ""
}
},
"parts_orders": { "parts_orders": {
"actions": { "actions": {
"backordered": "", "backordered": "",

View File

@@ -223,6 +223,7 @@
"reexport": "" "reexport": ""
}, },
"validation": { "validation": {
"closingperiod": "",
"inventoryquantity": "", "inventoryquantity": "",
"manualinhouse": "", "manualinhouse": "",
"unique_invoice_number": "" "unique_invoice_number": ""
@@ -262,6 +263,7 @@
"bill_local_tax_rate": "", "bill_local_tax_rate": "",
"bill_state_tax_rate": "", "bill_state_tax_rate": "",
"city": "", "city": "",
"closingperiod": "",
"country": "", "country": "",
"dailybodytarget": "", "dailybodytarget": "",
"dailypainttarget": "", "dailypainttarget": "",
@@ -1214,6 +1216,7 @@
"joblines": { "joblines": {
"actions": { "actions": {
"converttolabor": "", "converttolabor": "",
"dispatchparts": "",
"new": "" "new": ""
}, },
"errors": { "errors": {
@@ -1688,6 +1691,7 @@
"checklists": "", "checklists": "",
"closeconfirm": "", "closeconfirm": "",
"closejob": "", "closejob": "",
"closingperiod": "",
"contracts": "", "contracts": "",
"convertedtolabor": "", "convertedtolabor": "",
"cost": "", "cost": "",
@@ -1991,6 +1995,7 @@
"shops": "Mes boutiques" "shops": "Mes boutiques"
}, },
"tech": { "tech": {
"dispatchedparts": "",
"home": "", "home": "",
"jobclockin": "", "jobclockin": "",
"jobclockout": "", "jobclockout": "",
@@ -2131,6 +2136,27 @@
"orderinhouse": "" "orderinhouse": ""
} }
}, },
"parts_dispatch": {
"actions": {
"accept": ""
},
"errors": {
"accepting": "",
"creating": ""
},
"fields": {
"number": "",
"percent_accepted": ""
},
"labels": {
"parts_dispatch": ""
}
},
"parts_dispatch_lines": {
"fields": {
"accepted_at": ""
}
},
"parts_orders": { "parts_orders": {
"actions": { "actions": {
"backordered": "", "backordered": "",

View File

@@ -559,6 +559,15 @@ export const TemplateList = (type, context) => {
}), }),
disabled: false, disabled: false,
}, },
parts_dispatch: {
title: i18n.t("printcenter.jobs.parts_dispatch"),
description: "",
key: "parts_dispatch",
subject: i18n.t("printcenter.subjects.jobs.parts_dispatch", {
ro_number: (context && context.ro_number) || "",
}),
disabled: false,
},
} }
: {}), : {}),
...(!type || type === "appointment" ...(!type || type === "appointment"

View File

@@ -4649,6 +4649,7 @@
_eq: X-Hasura-User-Id _eq: X-Hasura-User-Id
- active: - active:
_eq: true _eq: true
allow_aggregations: true
update_permissions: update_permissions:
- role: user - role: user
permission: permission:
@@ -4726,6 +4727,7 @@
_eq: X-Hasura-User-Id _eq: X-Hasura-User-Id
- active: - active:
_eq: true _eq: true
allow_aggregations: true
update_permissions: update_permissions:
- role: user - role: user
permission: permission:

View File

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

View File

@@ -0,0 +1,2 @@
CREATE INDEX "parts_dispatch_employeeid" on
"public"."parts_dispatch" using btree ("employeeid");

View File

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

View File

@@ -0,0 +1,2 @@
CREATE INDEX "parts_dispatch_dispatchid" on
"public"."parts_dispatch_lines" using btree ("partsdispatchid");

View File

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

View File

@@ -0,0 +1,2 @@
CREATE INDEX "parts_dispatch_line_accepted_at" on
"public"."parts_dispatch_lines" using btree ("accepted_at");