Compare commits

..

2 Commits

Author SHA1 Message Date
Patrick Fic
b5a2be9392 Package updates. 2022-04-18 10:27:06 -07:00
Patrick Fic
df1e15be60 All package upgrades. 2022-04-13 17:46:46 -07:00
313 changed files with 71257 additions and 29771 deletions

View File

@@ -1,21 +0,0 @@
{
"env": {
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"rules": {
"no-console": "off"
},
"settings": {},
"plugins": ["cypress"]
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,56 +3,59 @@
"version": "0.2.1",
"private": true,
"proxy": "http://localhost:4000",
"browser": {
"fs": false,
"path": false,
"os": false
},
"dependencies": {
"@apollo/client": "^3.6.9",
"@apollo/client": "^3.5.10",
"@asseinfo/react-kanban": "^2.2.0",
"@craco/craco": "^6.4.5",
"@craco/craco": "^6.4.3",
"@fingerprintjs/fingerprintjs": "^3.3.3",
"@jsreport/browser-client": "^3.1.0",
"@sentry/react": "^7.7.0",
"@sentry/tracing": "^7.7.0",
"@splitsoftware/splitio-react": "^1.6.0",
"@stripe/react-stripe-js": "^1.9.0",
"@stripe/stripe-js": "^1.32.0",
"@tanem/react-nprogress": "^5.0.8",
"antd": "^4.22.3",
"@sentry/react": "^6.19.6",
"@sentry/tracing": "^6.19.6",
"@splitsoftware/splitio-react": "^1.4.0",
"@stripe/react-stripe-js": "^1.7.1",
"@stripe/stripe-js": "^1.27.0",
"@tanem/react-nprogress": "^5.0.0",
"antd": "^4.19.5",
"apollo-link-logger": "^2.0.0",
"axios": "^0.27.2",
"craco-less": "^1.20.0",
"axios": "^0.26.1",
"craco-less": "^2.0.0",
"dinero.js": "^1.9.1",
"dotenv": "^16.0.1",
"dotenv": "^16.0.0",
"enquire-js": "^0.2.1",
"env-cmd": "^10.1.0",
"exifr": "^7.1.3",
"firebase": "^9.9.1",
"graphql": "^16.5.0",
"i18next": "^21.8.14",
"firebase": "^9.6.10",
"graphql": "^16.3.0",
"i18next": "^21.6.16",
"i18next-browser-languagedetector": "^6.1.4",
"jsoneditor": "^9.9.0",
"jsreport-browser-client-dist": "^1.3.0",
"libphonenumber-js": "^1.10.9",
"logrocket": "^3.0.1",
"markerjs2": "^2.22.0",
"jsoneditor": "^9.7.4",
"libphonenumber-js": "^1.9.51",
"logrocket": "^2.2.1",
"markerjs2": "^2.21.0",
"moment-business-days": "^1.2.0",
"moment-timezone": "^0.5.34",
"normalize-url": "^7.0.3",
"phone": "^3.1.23",
"phone": "^3.1.15",
"preval.macro": "^5.0.0",
"prop-types": "^15.8.1",
"query-string": "^7.1.1",
"rc-queue-anim": "^2.0.0",
"rc-scroll-anim": "^2.7.6",
"react": "^17.0.2",
"react-big-calendar": "^1.5.0",
"react": "^18.0.0",
"react-big-calendar": "^0.40.0",
"react-color": "^2.19.3",
"react-cookie": "^4.1.1",
"react-dom": "^17.0.2",
"react-drag-listview": "^0.2.1",
"react-dom": "^18.0.0",
"react-drag-listview": "^0.1.9",
"react-grid-gallery": "^0.5.5",
"react-grid-layout": "^1.3.4",
"react-i18next": "^11.18.1",
"react-icons": "^4.4.0",
"react-number-format": "^4.9.3",
"react-i18next": "^11.16.6",
"react-icons": "^4.3.1",
"react-number-format": "^4.9.1",
"react-redux": "^7.2.8",
"react-resizable": "^3.0.4",
"react-router-dom": "^5.3.0",
@@ -60,14 +63,14 @@
"react-sticky": "^6.0.3",
"react-sublime-video": "^0.2.5",
"react-virtualized": "^9.22.3",
"recharts": "^2.1.12",
"redux": "^4.2.0",
"recharts": "^2.1.9",
"redux": "^4.1.2",
"redux-persist": "^6.0.0",
"redux-saga": "^1.1.3",
"redux-state-sync": "^3.1.4",
"reselect": "^4.1.6",
"sass": "^1.54.0",
"socket.io-client": "^4.5.1",
"redux-state-sync": "^3.1.2",
"reselect": "^4.1.5",
"sass": "^1.50.0",
"socket.io-client": "^4.4.1",
"styled-components": "^5.3.5",
"subscriptions-transport-ws": "^0.11.0",
"web-vitals": "^2.1.4",
@@ -119,9 +122,9 @@
"react-error-overlay": "6.0.9"
},
"devDependencies": {
"@sentry/webpack-plugin": "^1.19.0",
"@testing-library/cypress": "^8.0.3",
"cypress": "^10.3.1",
"@sentry/webpack-plugin": "^1.18.8",
"@testing-library/cypress": "^8.0.2",
"cypress": "^9.5.4",
"eslint-plugin-cypress": "^2.12.1",
"react-error-overlay": "6.0.11",
"redux-logger": "^3.0.6",

View File

@@ -142,9 +142,3 @@
}
}
}
//Update row highlighting on production board.
.ant-table-tbody > tr.ant-table-row:hover > td {
background: #eaeaea !important;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

View File

@@ -13,8 +13,6 @@ import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -29,12 +27,7 @@ export default connect(
mapDispatchToProps
)(AccountingPayablesTableComponent);
export function AccountingPayablesTableComponent({
bodyshop,
loading,
bills,
refetch,
}) {
export function AccountingPayablesTableComponent({ bodyshop, loading, bills }) {
const { t } = useTranslation();
const [selectedBills, setSelectedBills] = useState([]);
const [transInProgress, setTransInProgress] = useState(false);
@@ -138,9 +131,11 @@ export function AccountingPayablesTableComponent({
dataIndex: "attempts",
key: "attempts",
render: (text, record) => (
<ExportLogsCountDisplay logs={record.exportlogs} />
),
render: (text, record) => {
const success = record.exportlogs.filter((e) => e.successful).length;
const attempts = record.exportlogs.length;
return `${success}/${attempts}`;
},
},
{
title: t("general.labels.actions"),
@@ -149,13 +144,14 @@ export function AccountingPayablesTableComponent({
sorter: (a, b) => a.clm_total - b.clm_total,
render: (text, record) => (
<PayableExportButton
billId={record.id}
disabled={transInProgress || !!record.exported}
loadingCallback={setTransInProgress}
setSelectedBills={setSelectedBills}
refetch={refetch}
/>
<div>
<PayableExportButton
billId={record.id}
disabled={transInProgress || !!record.exported}
loadingCallback={setTransInProgress}
setSelectedBills={setSelectedBills}
/>
</div>
),
},
];
@@ -181,19 +177,11 @@ export function AccountingPayablesTableComponent({
<Card
extra={
<Space wrap>
<BillMarkSelectedExported
billids={selectedBills}
disabled={transInProgress || selectedBills.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedBills}
refetch={refetch}
/>
<PayableExportAll
billids={selectedBills}
disabled={transInProgress || selectedBills.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedBills}
refetch={refetch}
/>
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
<QboAuthorizeComponent />

View File

@@ -13,7 +13,6 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -32,7 +31,6 @@ export function AccountingPayablesTableComponent({
bodyshop,
loading,
payments,
refetch,
}) {
const { t } = useTranslation();
const [selectedPayments, setSelectedPayments] = useState([]);
@@ -132,9 +130,11 @@ export function AccountingPayablesTableComponent({
dataIndex: "attempts",
key: "attempts",
render: (text, record) => (
<ExportLogsCountDisplay logs={record.exportlogs} />
),
render: (text, record) => {
const success = record.exportlogs.filter((e) => e.successful).length;
const attempts = record.exportlogs.length;
return `${success}/${attempts}`;
},
},
{
title: t("general.labels.actions"),
@@ -148,7 +148,6 @@ export function AccountingPayablesTableComponent({
disabled={transInProgress || !!record.exportedat}
loadingCallback={setTransInProgress}
setSelectedPayments={setSelectedPayments}
refetch={refetch}
/>
),
},
@@ -189,7 +188,6 @@ export function AccountingPayablesTableComponent({
disabled={transInProgress || selectedPayments.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedPayments}
refetch={refetch}
/>
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
<QboAuthorizeComponent />

View File

@@ -14,7 +14,6 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import { DateFormatter } from "../../utils/DateFormatter";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -31,7 +30,6 @@ export function AccountingReceivablesTableComponent({
bodyshop,
loading,
jobs,
refetch,
}) {
const { t } = useTranslation();
const [selectedJobs, setSelectedJobs] = useState([]);
@@ -141,9 +139,12 @@ export function AccountingReceivablesTableComponent({
title: t("exportlogs.labels.attempts"),
dataIndex: "attempts",
key: "attempts",
render: (text, record) => (
<ExportLogsCountDisplay logs={record.exportlogs} />
),
render: (text, record) => {
const success = record.exportlogs.filter((e) => e.successful).length;
const attempts = record.exportlogs.length;
return `${success}/${attempts}`;
},
},
{
title: t("general.labels.actions"),
@@ -156,7 +157,6 @@ export function AccountingReceivablesTableComponent({
jobId={record.id}
disabled={!!record.date_exported}
setSelectedJobs={setSelectedJobs}
refetch={refetch}
/>
<Link to={`/manage/jobs/${record.id}/close`}>
<Button>{t("jobs.labels.viewallocations")}</Button>
@@ -207,7 +207,6 @@ export function AccountingReceivablesTableComponent({
disabled={transInProgress || selectedJobs.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedJobs}
refetch={refetch}
/>
)}
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (

View File

@@ -1,135 +0,0 @@
import { Checkbox, Form, Skeleton, Typography } from "antd";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
import "./bill-cm-returns-table.styles.scss";
export default function BillCmdReturnsTableComponent({
form,
returnLoading,
returnData,
}) {
const { t } = useTranslation();
useEffect(() => {
if (returnData) {
form.setFieldsValue({
outstanding_returns: returnData.parts_order_lines,
});
}
}, [returnData, form]);
return (
<Form.Item
shouldUpdate={(prev, cur) =>
prev.jobid !== cur.jobid ||
prev.is_credit_memo !== cur.is_credit_memo ||
prev.vendorid !== cur.vendorid
}
noStyle
>
{() => {
const isReturn = form.getFieldValue("is_credit_memo");
if (!isReturn) {
return null;
}
if (returnLoading) return <Skeleton />;
return (
<Form.List name="outstanding_returns">
{(fields, { add, remove, move }) => {
return (
<>
<Typography.Title level={4}>
{t("bills.labels.creditsnotreceived")}
</Typography.Title>
<table className="bill-cm-returns-table">
<thead>
<tr>
<th>{t("parts_orders.fields.line_desc")}</th>
<th>{t("parts_orders.fields.part_type")}</th>
<th>{t("parts_orders.fields.quantity")}</th>
<th>{t("parts_orders.fields.act_price")}</th>
<th>{t("parts_orders.fields.cost")}</th>
<th>{t("parts_orders.labels.mark_as_received")}</th>
</tr>
</thead>
<tbody>
{fields.map((field, index) => (
<tr key={field.key}>
<td>
<Form.Item
// label={t("joblines.fields.line_desc")}
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}part_type`}
name={[field.name, "part_type"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}quantity`}
name={[field.name, "quantity"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}act_price`}
name={[field.name, "act_price"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}cost`}
name={[field.name, "cost"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}cm_received`}
name={[field.name, "cm_received"]}
valuePropName="checked"
>
<Checkbox />
</Form.Item>
</td>
</tr>
))}
</tbody>
</table>
</>
);
}}
</Form.List>
);
}}
</Form.Item>
);
}

View File

@@ -1,19 +0,0 @@
.bill-cm-returns-table {
table-layout: fixed;
width: 100%;
th,
td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #ddd;
.ant-form-item {
margin-bottom: 0px !important;
}
}
tr:hover {
background-color: #f5f5f5;
}
}

View File

@@ -15,8 +15,7 @@ export default function BillDeleteButton({ bill }) {
setLoading(true);
const result = await deleteBill({
variables: { billId: bill.id },
update(cache, { errors }) {
if (errors) return;
update(cache) {
cache.modify({
fields: {
bills(existingBills, { readField }) {
@@ -37,22 +36,11 @@ export default function BillDeleteButton({ bill }) {
if (!!!result.errors) {
notification["success"]({ message: t("bills.successes.deleted") });
} else {
//Check if it's an fkey violation.
const error = JSON.stringify(result.errors);
if (error.toLowerCase().includes("inventory_billid_fkey")) {
notification["error"]({
message: t("bills.errors.deleting", {
error: t("bills.errors.existinginventoryline"),
}),
});
} else {
notification["error"]({
message: t("bills.errors.deleting", {
error: JSON.stringify(result.errors),
}),
});
}
notification["error"]({
message: t("bills.errors.deleting", {
error: JSON.stringify(result.errors),
}),
});
}
setLoading(false);

View File

@@ -1,251 +0,0 @@
import { useMutation, useQuery } from "@apollo/client";
import { Button, Form, PageHeader, Popconfirm, Space } from "antd";
import moment from "moment";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import {
DELETE_BILL_LINE,
INSERT_NEW_BILL_LINES,
UPDATE_BILL_LINE
} from "../../graphql/bill-lines.queries";
import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import AlertComponent from "../alert/alert.component";
import BillFormContainer from "../bill-form/bill-form.container";
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component";
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import BillDetailEditReturn from "./bill-detail-edit-return.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillDetailEditcontainer);
export function BillDetailEditcontainer({
setPartsOrderContext,
insertAuditTrail,
bodyshop,
}) {
const search = queryString.parse(useLocation().search);
const { t } = useTranslation();
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const [updateLoading, setUpdateLoading] = useState(false);
const [update_bill] = useMutation(UPDATE_BILL);
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
const { loading, error, data, refetch } = useQuery(QUERY_BILL_BY_PK, {
variables: { billid: search.billid },
skip: !!!search.billid,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const handleSave = () => {
//It's got a previously deducted bill line!
if (
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
0
)
setVisible(true);
else {
form.submit();
}
};
const handleFinish = async (values) => {
setUpdateLoading(true);
//let adjustmentsToInsert = {};
const { billlines, upload, ...bill } = values;
const updates = [];
updates.push(
update_bill({
variables: { billId: search.billid, bill: bill },
})
);
billlines.forEach((l) => {
delete l.selected;
});
//Find bill lines that were deleted.
const deletedJobLines = [];
data.bills_by_pk.billlines.forEach((a) => {
const matchingRecord = billlines.find((b) => b.id === a.id);
if (!matchingRecord) {
deletedJobLines.push(a);
}
});
deletedJobLines.forEach((d) => {
updates.push(deleteBillLine({ variables: { id: d.id } }));
});
billlines.forEach((billline) => {
const { deductedfromlbr, inventories, jobline, ...il } = billline;
delete il.__typename;
if (il.id) {
updates.push(
updateBillLine({
variables: {
billLineId: il.id,
billLine: {
...il,
deductedfromlbr: deductedfromlbr,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
},
})
);
} else {
//It's a new line, have to insert it.
updates.push(
insertBillLine({
variables: {
billLines: [
{
...il,
deductedfromlbr: deductedfromlbr,
billid: search.billid,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
],
},
})
);
}
});
await Promise.all(updates);
insertAuditTrail({
jobid: bill.jobid,
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
});
await refetch();
form.setFieldsValue(transformData(data));
form.resetFields();
setVisible(false);
setUpdateLoading(false);
};
if (error) return <AlertComponent message={error.message} type="error" />;
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
return (
<>
{loading && <LoadingSkeleton />}
{data && (
<>
<PageHeader
title={
data &&
`${data.bills_by_pk.invoice_number} - ${data.bills_by_pk.vendor.name}`
}
extra={
<Space>
<BillDetailEditReturn data={data} />
<Popconfirm
visible={visible}
onConfirm={() => form.submit()}
onCancel={() => setVisible(false)}
okButtonProps={{ loading: updateLoading }}
title={t("bills.labels.editadjwarning")}
>
<Button
htmlType="submit"
disabled={exported}
onClick={handleSave}
loading={updateLoading}
type="primary"
>
{t("general.actions.save")}
</Button>
</Popconfirm>
<BillReeportButtonComponent bill={data && data.bills_by_pk} />
<BillMarkExportedButton bill={data && data.bills_by_pk} />
</Space>
}
/>
<Form
form={form}
onFinish={handleFinish}
initialValues={transformData(data)}
layout="vertical"
>
<BillFormContainer form={form} billEdit disabled={exported} />
{bodyshop.uselocalmediaserver ? (
<JobsDocumentsLocalGallery
job={{ id: data ? data.bills_by_pk.jobid : null }}
invoice_number={data ? data.bills_by_pk.invoice_number : null}
vendorid={data ? data.bills_by_pk.vendorid : null}
/>
) : (
<JobDocumentsGallery
jobId={data ? data.bills_by_pk.jobid : null}
billId={search.billid}
documentsList={data ? data.bills_by_pk.documents : []}
billsCallback={refetch}
/>
)}
</Form>
</>
)}
</>
);
}
const transformData = (data) => {
return data
? {
...data.bills_by_pk,
billlines: data.bills_by_pk.billlines.map((i) => {
return {
...i,
joblineid: !!i.joblineid ? i.joblineid : "noline",
applicable_taxes: {
federal:
(i.applicable_taxes && i.applicable_taxes.federal) || false,
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
local: (i.applicable_taxes && i.applicable_taxes.local) || false,
},
};
}),
date: data.bills_by_pk ? moment(data.bills_by_pk.date) : null,
}
: {};
};

View File

@@ -1,185 +0,0 @@
import { Button, Checkbox, Form, Modal } from "antd";
import queryString from "query-string";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillDetailEditReturn);
export function BillDetailEditReturn({
setPartsOrderContext,
insertAuditTrail,
bodyshop,
data,
disabled,
}) {
const search = queryString.parse(useLocation().search);
const history = useHistory();
const { t } = useTranslation();
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const handleFinish = ({ billlines }) => {
const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id);
setPartsOrderContext({
actions: {},
context: {
jobId: data.bills_by_pk.jobid,
vendorId: data.bills_by_pk.vendorid,
returnFromBill: data.bills_by_pk.id,
invoiceNumber: data.bills_by_pk.invoice_number,
linesToOrder: data.bills_by_pk.billlines
.filter((l) => selectedLines.includes(l.id))
.map((i) => {
return {
line_desc: i.line_desc,
// db_price: i.actual_price,
act_price: i.actual_price,
cost: i.actual_cost,
quantity: i.quantity,
joblineid: i.joblineid,
oem_partno: i.jobline && i.jobline.oem_partno,
part_type: i.jobline && i.jobline.part_type,
};
}),
isReturn: true,
},
});
delete search.billid;
history.push({ search: queryString.stringify(search) });
setVisible(false);
};
useEffect(() => {
if (visible === false) form.resetFields();
}, [visible, form]);
return (
<>
<Modal
visible={visible}
onCancel={() => setVisible(false)}
destroyOnClose
title={t("bills.actions.return")}
onOk={() => form.submit()}
>
<Form
initialValues={data && data.bills_by_pk}
onFinish={handleFinish}
form={form}
>
<Form.List name={["billlines"]}>
{(fields, { add, remove, move }) => {
return (
<table style={{ tableLayout: "auto", width: "100%" }}>
<thead>
<tr>
<td>
<Checkbox
onChange={(e) => {
form.setFieldsValue({
billlines: form
.getFieldsValue()
.billlines.map((b) => ({
...b,
selected: e.target.checked,
})),
});
}}
/>
</td>
<td>{t("billlines.fields.line_desc")}</td>
<td>{t("billlines.fields.quantity")}</td>
<td>{t("billlines.fields.actual_price")}</td>
<td>{t("billlines.fields.actual_cost")}</td>
</tr>
</thead>
<tbody>
{fields.map((field, index) => (
<tr key={field.key}>
<td>
<Form.Item
// label={t("joblines.fields.selected")}
key={`${index}selected`}
name={[field.name, "selected"]}
valuePropName="checked"
>
<Checkbox />
</Form.Item>
</td>
<td>
<Form.Item
// label={t("joblines.fields.line_desc")}
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
// label={t("joblines.fields.quantity")}
key={`${index}quantity`}
name={[field.name, "quantity"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
// label={t("joblines.fields.actual_price")}
key={`${index}actual_price`}
name={[field.name, "actual_price"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
<td>
<Form.Item
// label={t("joblines.fields.actual_cost")}
key={`${index}actual_cost`}
name={[field.name, "actual_cost"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
</tr>
))}
</tbody>
</table>
);
}}
</Form.List>
</Form>
</Modal>
<Button
disabled={data.bills_by_pk.is_credit_memo || disabled}
onClick={() => {
setVisible(true);
}}
>
{t("bills.actions.return")}
</Button>
</>
);
}

View File

@@ -1,12 +1,65 @@
import { Drawer, Grid } from "antd";
import { useMutation, useQuery } from "@apollo/client";
import {
Button,
Drawer,
Form,
Grid,
PageHeader,
Popconfirm,
Space,
} from "antd";
import moment from "moment";
import queryString from "query-string";
import React from "react";
import { useHistory, useLocation } from "react-router-dom";
import BillDetailEditComponent from "./bill-detail-edit-component";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLocation, useHistory } from "react-router-dom";
import {
DELETE_BILL_LINE,
INSERT_NEW_BILL_LINES,
UPDATE_BILL_LINE,
} from "../../graphql/bill-lines.queries";
import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
import AlertComponent from "../alert/alert.component";
import BillFormContainer from "../bill-form/bill-form.container";
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
export default function BillDetailEditcontainer() {
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillDetailEditcontainer);
export function BillDetailEditcontainer({
setPartsOrderContext,
insertAuditTrail,
}) {
const search = queryString.parse(useLocation().search);
const history = useHistory();
const { t } = useTranslation();
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const [updateLoading, setUpdateLoading] = useState(false);
const [update_bill] = useMutation(UPDATE_BILL);
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
@@ -17,13 +70,121 @@ export default function BillDetailEditcontainer() {
sm: "100%",
md: "100%",
lg: "100%",
xl: "90%",
xxl: "90%",
xl: "80%",
xxl: "80%",
};
const drawerPercentage = selectedBreakpoint
? bpoints[selectedBreakpoint[0]]
: "100%";
const { loading, error, data, refetch } = useQuery(QUERY_BILL_BY_PK, {
variables: { billid: search.billid },
skip: !!!search.billid,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const handleSave = () => {
//It's got a previously deducted bill line!
if (
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
0
)
setVisible(true);
else {
form.submit();
}
};
const handleFinish = async (values) => {
setUpdateLoading(true);
//let adjustmentsToInsert = {};
const { billlines, upload, ...bill } = values;
const updates = [];
updates.push(
update_bill({
variables: { billId: search.billid, bill: bill },
})
);
//Find bill lines that were deleted.
const deletedJobLines = [];
data.bills_by_pk.billlines.forEach((a) => {
const matchingRecord = billlines.find((b) => b.id === a.id);
if (!matchingRecord) {
deletedJobLines.push(a);
}
});
deletedJobLines.forEach((d) => {
updates.push(deleteBillLine({ variables: { id: d.id } }));
});
billlines.forEach((billline) => {
const { deductedfromlbr, jobline, ...il } = billline;
delete il.__typename;
if (il.id) {
updates.push(
updateBillLine({
variables: {
billLineId: il.id,
billLine: {
...il,
deductedfromlbr: deductedfromlbr,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
},
})
);
} else {
//It's a new line, have to insert it.
updates.push(
insertBillLine({
variables: {
billLines: [
{
...il,
deductedfromlbr: deductedfromlbr,
billid: search.billid,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
],
},
})
);
}
});
await Promise.all(updates);
insertAuditTrail({
jobid: bill.jobid,
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
});
await refetch();
form.setFieldsValue(transformData(data));
form.resetFields();
setVisible(false);
setUpdateLoading(false);
};
useEffect(() => {
if (search.billid && data) {
form.resetFields();
}
}, [form, search.billid, data]);
if (error) return <AlertComponent message={error.message} type="error" />;
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
return (
<Drawer
width={drawerPercentage}
@@ -31,10 +192,110 @@ export default function BillDetailEditcontainer() {
delete search.billid;
history.push({ search: queryString.stringify(search) });
}}
destroyOnClose
visible={search.billid}
>
<BillDetailEditComponent />
{loading && <LoadingSkeleton />}
{!loading && (
<>
<PageHeader
title={
data &&
`${data.bills_by_pk.invoice_number} - ${data.bills_by_pk.vendor.name}`
}
extra={
<Space>
<Button
disabled={data.bills_by_pk.is_credit_memo}
onClick={() => {
delete search.billid;
history.push({ search: queryString.stringify(search) });
setPartsOrderContext({
actions: {},
context: {
jobId: data.bills_by_pk.jobid,
vendorId: data.bills_by_pk.vendorid,
returnFromBill: data.bills_by_pk.id,
invoiceNumber: data.bills_by_pk.invoice_number,
linesToOrder: data.bills_by_pk.billlines.map((i) => {
return {
line_desc: i.line_desc,
// db_price: i.actual_price,
act_price: i.actual_price,
cost: i.actual_cost,
quantity: i.quantity,
joblineid: i.joblineid,
oem_partno: i.jobline && i.jobline.oem_partno,
part_type: i.jobline && i.jobline.part_type,
};
}),
isReturn: true,
},
});
}}
>
{t("bills.actions.return")}
</Button>
<Popconfirm
visible={visible}
onConfirm={() => form.submit()}
onCancel={() => setVisible(false)}
okButtonProps={{ loading: updateLoading }}
title={t("bills.labels.editadjwarning")}
>
<Button
htmlType="submit"
disabled={exported}
onClick={handleSave}
loading={updateLoading}
type="primary"
>
{t("general.actions.save")}
</Button>
</Popconfirm>
<BillReeportButtonComponent bill={data && data.bills_by_pk} />
<BillMarkExportedButton bill={data && data.bills_by_pk} />
</Space>
}
/>
<Form
form={form}
onFinish={handleFinish}
initialValues={transformData(data)}
layout="vertical"
>
<BillFormContainer form={form} billEdit disabled={exported} />
<JobDocumentsGallery
jobId={data ? data.bills_by_pk.jobid : null}
billId={search.billid}
documentsList={data ? data.bills_by_pk.documents : []}
billsCallback={refetch}
/>
</Form>
</>
)}
</Drawer>
);
}
const transformData = (data) => {
return data
? {
...data.bills_by_pk,
billlines: data.bills_by_pk.billlines.map((i) => {
return {
...i,
joblineid: !!i.joblineid ? i.joblineid : "noline",
applicable_taxes: {
federal:
(i.applicable_taxes && i.applicable_taxes.federal) || false,
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
local: (i.applicable_taxes && i.applicable_taxes.local) || false,
},
};
}),
date: data.bills_by_pk ? moment(data.bills_by_pk.date) : null,
}
: {};
};

View File

@@ -11,8 +11,6 @@ import {
QUERY_JOB_LBR_ADJUSTMENTS,
UPDATE_JOB,
} from "../../graphql/jobs.queries";
import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries";
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
@@ -25,7 +23,6 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
import BillFormContainer from "../bill-form/bill-form.container";
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
import { handleUpload } from "../documents-upload/documents-upload.utility";
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
const mapStateToProps = createStructuredSelector({
billEnterModal: selectBillEnterModal,
@@ -50,8 +47,6 @@ function BillEnterModalContainer({
const [enterAgain, setEnterAgain] = useState(false);
const [insertBill] = useMutation(INSERT_NEW_BILL);
const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
const [updatePartsOrderLines] = useMutation(MUTATION_MARK_RETURN_RECEIVED);
const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES);
const [loading, setLoading] = useState(false);
const client = useApolloClient();
@@ -81,13 +76,7 @@ function BillEnterModalContainer({
}
setLoading(true);
const {
upload,
location,
outstanding_returns,
inventory,
...remainingValues
} = values;
const { upload, location, ...remainingValues } = values;
let adjustmentsToInsert = {};
@@ -144,14 +133,6 @@ function BillEnterModalContainer({
adjKeys.forEach((key) => {
newAdjustments[key] =
(newAdjustments[key] || 0) + adjustmentsToInsert[key];
insertAuditTrail({
jobid: values.jobid,
operation: AuditTrailMapping.jobmodifylbradj({
mod_lbr_ty: key,
hours: adjustmentsToInsert[key].toFixed(1),
}),
});
});
const jobUpdate = client.mutate({
@@ -169,25 +150,10 @@ function BillEnterModalContainer({
});
return;
}
}
const markPolReceived =
outstanding_returns &&
outstanding_returns.filter((o) => o.cm_received === true);
if (markPolReceived && markPolReceived.length > 0) {
const r2 = await updatePartsOrderLines({
variables: { partsLineIds: markPolReceived.map((p) => p.id) },
insertAuditTrail({
jobid: values.jobid,
operation: AuditTrailMapping.jobmodifylbradj(),
});
if (!!r2.errors) {
setLoading(false);
setEnterAgain(false);
notification["error"]({
message: t("parts_orders.errors.updating", {
message: JSON.stringify(r2.errors),
}),
});
}
}
if (!!r1.errors) {
@@ -201,26 +167,6 @@ function BillEnterModalContainer({
}
const billId = r1.data.insert_bills.returning[0].id;
const markInventoryConsumed =
inventory && inventory.filter((i) => i.consumefrominventory);
if (markInventoryConsumed && markInventoryConsumed.length > 0) {
const r2 = await updateInventoryLines({
variables: {
InventoryIds: markInventoryConsumed.map((p) => p.id),
consumedbybillid: billId,
},
});
if (!!r2.errors) {
setLoading(false);
setEnterAgain(false);
notification["error"]({
message: t("inventory.errors.updating", {
message: JSON.stringify(r2.errors),
}),
});
}
}
await Promise.all(
remainingValues.billlines
@@ -242,33 +188,19 @@ function BillEnterModalContainer({
/////////////////////////
if (upload && upload.length > 0) {
//insert Each of the documents?
if (bodyshop.uselocalmediaserver) {
upload.forEach((u) => {
handleLocalUpload({
ev: { file: u.originFileObj },
context: {
jobid: values.jobid,
invoice_number: remainingValues.invoice_number,
vendorid: remainingValues.vendorid,
},
});
});
} else {
upload.forEach((u) => {
handleUpload(
{ file: u.originFileObj },
{
bodyshop: bodyshop,
uploaded_by: currentUser.email,
jobId: values.jobid,
billId: billId,
tagsArray: null,
callback: null,
}
);
});
}
upload.forEach((u) => {
handleUpload(
{ file: u.originFileObj },
{
bodyshop: bodyshop,
uploaded_by: currentUser.email,
jobId: values.jobid,
billId: billId,
tagsArray: null,
callback: null,
}
);
});
}
///////////////////////////
setLoading(false);

View File

@@ -47,8 +47,6 @@ export function BillFormComponent({
billEdit,
disableInvNumber,
job,
loadOutstandingReturns,
loadInventory,
}) {
const { t } = useTranslation();
const client = useApolloClient();
@@ -58,18 +56,8 @@ export function BillFormComponent({
{},
bodyshop.imexshopid
);
const handleVendorSelect = (props, opt) => {
setDiscount(opt.discount);
opt &&
!billEdit &&
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: opt.value,
},
});
};
useEffect(() => {
@@ -77,8 +65,8 @@ export function BillFormComponent({
}, [job, form]);
useEffect(() => {
const vendorId = form.getFieldValue("vendorid");
if (vendorId && vendorAutoCompleteOptions) {
if (form.getFieldValue("vendorid") && vendorAutoCompleteOptions) {
const vendorId = form.getFieldValue("vendorid");
const matchingVendors = vendorAutoCompleteOptions.filter(
(v) => v.id === vendorId
);
@@ -86,32 +74,10 @@ export function BillFormComponent({
setDiscount(matchingVendors[0].discount);
}
}
const jobId = form.getFieldValue("jobid");
if (jobId) {
loadLines({ variables: { id: jobId } });
if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) {
loadOutstandingReturns({
variables: {
jobId: jobId,
vendorId: vendorId,
},
});
}
if (form.getFieldValue("jobid")) {
loadLines({ variables: { id: form.getFieldValue("jobid") } });
}
if (vendorId === bodyshop.inhousevendorid && !billEdit) {
loadInventory();
}
}, [
form,
billEdit,
loadOutstandingReturns,
loadInventory,
setDiscount,
vendorAutoCompleteOptions,
loadLines,
bodyshop.inhousevendorid,
]);
}, [form, setDiscount, vendorAutoCompleteOptions, loadLines]);
return (
<div>
@@ -141,14 +107,6 @@ export function BillFormComponent({
onBlur={() => {
if (form.getFieldValue("jobid") !== null) {
loadLines({ variables: { id: form.getFieldValue("jobid") } });
if (form.getFieldValue("vendorid") !== null) {
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: form.getFieldValue("vendorid"),
},
});
}
}
}}
/>
@@ -270,23 +228,8 @@ export function BillFormComponent({
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
if (
value === true &&
getFieldValue("jobid") &&
getFieldValue("vendorid")
) {
//Removed as this would cause an additional reload when validating the form on submit and clear the values.
// loadOutstandingReturns({
// variables: {
// jobId: form.getFieldValue("jobid"),
// vendorId: form.getFieldValue("vendorid"),
// },
// });
}
if (
!bodyshop.bill_allow_post_to_closed &&
job &&
(job.status === bodyshop.md_ro_statuses.default_invoiced ||
job.status === bodyshop.md_ro_statuses.default_exported ||
job.status === bodyshop.md_ro_statuses.default_void) &&
@@ -314,17 +257,15 @@ export function BillFormComponent({
>
<CurrencyInput min={0} disabled={disabled} />
</Form.Item>
{!billEdit && (
<Form.Item label={t("bills.fields.allpartslocation")} name="location">
<Select style={{ width: "10rem" }} disabled={disabled} allowClear>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
</Form.Item>
)}
<Form.Item label={t("bills.fields.allpartslocation")} name="location">
<Select style={{ width: "10rem" }} disabled={disabled} allowClear>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
</Form.Item>
</LayoutFormRow>
<LayoutFormRow>
<Form.Item
@@ -439,7 +380,6 @@ export function BillFormComponent({
form={form}
responsibilityCenters={responsibilityCenters}
disabled={disabled}
billEdit={billEdit}
/>
)}

View File

@@ -6,11 +6,6 @@ import { GET_JOB_LINES_TO_ENTER_BILL } from "../../graphql/jobs-lines.queries";
import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import BillFormComponent from "./bill-form.component";
import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component";
import { QUERY_UNRECEIVED_LINES } from "../../graphql/parts-orders.queries";
import BillInventoryTable from "../bill-inventory-table/bill-inventory-table.component";
import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";
import { useTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -23,12 +18,6 @@ export function BillFormContainer({
disabled,
disableInvNumber,
}) {
const { Simple_Inventory } = useTreatments(
["Simple_Inventory"],
{},
bodyshop && bodyshop.imexshopid
);
const { data: VendorAutoCompleteData } = useQuery(
SEARCH_VENDOR_AUTOCOMPLETE,
{ fetchPolicy: "network-only", nextFetchPolicy: "network-only" }
@@ -38,44 +27,20 @@ export function BillFormContainer({
GET_JOB_LINES_TO_ENTER_BILL
);
const [loadOutstandingReturns, { loading: returnLoading, data: returnData }] =
useLazyQuery(QUERY_UNRECEIVED_LINES);
const [loadInventory, { loading: inventoryLoading, data: inventoryData }] =
useLazyQuery(QUERY_OUTSTANDING_INVENTORY);
return (
<>
<BillFormComponent
disabled={disabled}
form={form}
billEdit={billEdit}
vendorAutoCompleteOptions={
VendorAutoCompleteData && VendorAutoCompleteData.vendors
}
loadLines={loadLines}
lineData={lineData ? lineData.joblines : []}
job={lineData ? lineData.jobs_by_pk : null}
responsibilityCenters={bodyshop.md_responsibility_centers || null}
disableInvNumber={disableInvNumber}
loadOutstandingReturns={loadOutstandingReturns}
loadInventory={loadInventory}
/>
{!billEdit && (
<BillCmdReturnsTableComponent
form={form}
returnLoading={returnLoading}
returnData={returnData}
/>
)}
{Simple_Inventory.treatment === "on" && (
<BillInventoryTable
form={form}
inventoryLoading={inventoryLoading}
inventoryData={billEdit ? [] : inventoryData}
billEdit={billEdit}
/>
)}
</>
<BillFormComponent
disabled={disabled}
form={form}
billEdit={billEdit}
vendorAutoCompleteOptions={
VendorAutoCompleteData && VendorAutoCompleteData.vendors
}
loadLines={loadLines}
lineData={lineData ? lineData.joblines : []}
job={lineData ? lineData.jobs_by_pk : null}
responsibilityCenters={bodyshop.md_responsibility_centers || null}
disableInvNumber={disableInvNumber}
/>
);
}
export default connect(mapStateToProps, null)(BillFormContainer);

View File

@@ -1,7 +1,7 @@
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
import { useTreatments } from "@splitsoftware/splitio-react";
import {
Button, Form,
Button,
Form,
Input,
InputNumber,
Select,
@@ -17,7 +17,6 @@ import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CiecaSelect from "../../utils/Ciecaselect";
import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.component";
import BilllineAddInventory from "../billline-add-inventory/billline-add-inventory.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
const mapStateToProps = createStructuredSelector({
@@ -35,16 +34,10 @@ export function BillEnterModalLinesComponent({
discount,
form,
responsibilityCenters,
billEdit,
billid,
}) {
const { t } = useTranslation();
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
const { Simple_Inventory } = useTreatments(
["Simple_Inventory"],
{},
bodyshop && bodyshop.imexshopid
);
const columns = (remove) => {
return [
{
@@ -149,24 +142,6 @@ export function BillEnterModalLinesComponent({
required: true,
//message: t("general.validation.required"),
},
({ getFieldValue }) => ({
validator(rule, value) {
if (
value &&
getFieldValue("billlines")[field.fieldKey]?.inventories
?.length > value
) {
return Promise.reject(
t("bills.validation.inventoryquantity", {
number:
getFieldValue("billlines")[field.fieldKey]
?.inventories?.length,
})
);
}
return Promise.resolve();
},
}),
],
};
},
@@ -322,31 +297,28 @@ export function BillEnterModalLinesComponent({
</Select>
),
},
...(billEdit
? []
: [
{
title: t("billlines.fields.location"),
dataIndex: "location",
editable: true,
label: t("billlines.fields.location"),
formItemProps: (field) => {
return {
key: `${field.index}location`,
name: [field.name, "location"],
};
},
formInput: (record, index) => (
<Select disabled={disabled}>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
),
},
]),
{
title: t("billlines.fields.location"),
dataIndex: "location",
editable: true,
label: t("billlines.fields.location"),
formItemProps: (field) => {
return {
key: `${field.index}location`,
name: [field.name, "location"],
};
},
formInput: (record, index) => (
<Select disabled={disabled}>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
),
},
{
title: t("billlines.labels.deductedfromlbr"),
dataIndex: "deductedfromlbr",
@@ -505,33 +477,9 @@ export function BillEnterModalLinesComponent({
dataIndex: "actions",
render: (text, record) => (
<Form.Item shouldUpdate noStyle>
{() => (
<Space wrap>
<Button
disabled={
disabled ||
getFieldValue("billlines")[record.fieldKey]?.inventories
?.length > 0
}
onClick={() => remove(record.name)}
>
<DeleteFilled />
</Button>
{Simple_Inventory.treatment === "on" && (
<BilllineAddInventory
disabled={
!billEdit ||
form.isFieldsTouched() ||
form.getFieldValue("is_credit_memo")
}
billline={getFieldValue("billlines")[record.fieldKey]}
jobid={getFieldValue("jobid")}
/>
)}
</Space>
)}
</Form.Item>
<Button disabled={disabled} onClick={() => remove(record.name)}>
<DeleteFilled />
</Button>
),
},
];
@@ -554,20 +502,7 @@ export function BillEnterModalLinesComponent({
});
return (
<Form.List
name="billlines"
rules={[
{
validator: async (_, billlines) => {
if (!billlines || billlines.length < 1) {
return Promise.reject(
new Error(t("billlines.validation.atleastone"))
);
}
},
},
]}
>
<Form.List name="billlines">
{(fields, { add, remove, move }) => {
return (
<>

View File

@@ -1,173 +0,0 @@
import { Checkbox, Form, Skeleton, Typography } from "antd";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
import "./bill-inventory-table.styles.scss";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
billEnterModal: selectBillEnterModal,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(BillInventoryTable);
export function BillInventoryTable({
billEnterModal,
bodyshop,
form,
billEdit,
inventoryLoading,
inventoryData,
}) {
const { t } = useTranslation();
useEffect(() => {
if (inventoryData && inventoryData.inventory) {
form.setFieldsValue({
inventory: billEnterModal.context.consumeinventoryid
? inventoryData.inventory.map((i) => {
if (i.id === billEnterModal.context.consumeinventoryid)
i.consumefrominventory = true;
return i;
})
: inventoryData.inventory,
});
}
}, [inventoryData, form, billEnterModal.context.consumeinventoryid]);
return (
<Form.Item
shouldUpdate={(prev, cur) => prev.vendorid !== cur.vendorid}
noStyle
>
{() => {
const is_inhouse =
form.getFieldValue("vendorid") === bodyshop.inhousevendorid;
if (!is_inhouse || billEdit) {
return null;
}
if (inventoryLoading) return <Skeleton />;
return (
<Form.List name="inventory">
{(fields, { add, remove, move }) => {
return (
<>
<Typography.Title level={4}>
{t("inventory.labels.inventory")}
</Typography.Title>
<table className="bill-inventory-table">
<thead>
<tr>
<th>{t("billlines.fields.line_desc")}</th>
<th>{t("vendors.fields.name")}</th>
<th>{t("billlines.fields.quantity")}</th>
<th>{t("billlines.fields.actual_price")}</th>
<th>{t("billlines.fields.actual_cost")}</th>
<th>{t("inventory.fields.comment")}</th>
<th>{t("inventory.actions.consumefrominventory")}</th>
</tr>
</thead>
<tbody>
{fields.map((field, index) => (
<tr key={field.key}>
<td>
<Form.Item
// label={t("joblines.fields.line_desc")}
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}part_type`}
name={[
field.name,
"billline",
"bill",
"vendor",
"name",
]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}quantity`}
name={[field.name, "quantity"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}act_price`}
name={[field.name, "actual_price"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}cost`}
name={[field.name, "actual_cost"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}comment`}
name={[field.name, "comment"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}consumefrominventory`}
name={[field.name, "consumefrominventory"]}
valuePropName="checked"
>
<Checkbox />
</Form.Item>
</td>
</tr>
))}
</tbody>
</table>
</>
);
}}
</Form.List>
);
}}
</Form.Item>
);
}

View File

@@ -1,19 +0,0 @@
.bill-inventory-table {
table-layout: fixed;
width: 100%;
th,
td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #ddd;
.ant-form-item {
margin-bottom: 0px !important;
}
}
tr:hover {
background-color: #f5f5f5;
}
}

View File

@@ -27,10 +27,6 @@ const BillLineSearchSelect = (
option.oem_partno
.toLowerCase()
.includes(inputValue.toLowerCase())) ||
(option.alt_partno &&
option.alt_partno
.toLowerCase()
.includes(inputValue.toLowerCase())) ||
(option.act_price &&
option.act_price.toString().startsWith(inputValue.toString()))
);
@@ -52,17 +48,14 @@ const BillLineSearchSelect = (
line_desc={item.line_desc}
part_qty={item.part_qty}
oem_partno={item.oem_partno}
alt_partno={item.alt_partno}
act_price={item.act_price}
style={{
...(item.removed ? { textDecoration: "line-through" } : {}),
}}
>
<span>
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
item.oem_partno ? ` - ${item.oem_partno}` : ""
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()}
</span>
<span>{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
item.oem_partno ? ` - ${item.oem_partno}` : ""
}`}</span>
<span style={{ float: "right", paddingleft: "1rem" }}>
{item.act_price
? `$${item.act_price && item.act_price.toFixed(2)}`

View File

@@ -9,14 +9,11 @@ import { createStructuredSelector } from "reselect";
import {
selectAuthLevel,
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
authLevel: selectAuthLevel,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
@@ -27,15 +24,9 @@ export default connect(
mapDispatchToProps
)(BillMarkExportedButton);
export function BillMarkExportedButton({
currentUser,
bodyshop,
authLevel,
bill,
}) {
export function BillMarkExportedButton({ bodyshop, authLevel, bill }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
const [updateBill] = useMutation(gql`
mutation UPDATE_BILL($billId: uuid!) {
@@ -55,20 +46,6 @@ export function BillMarkExportedButton({
variables: { billId: bill.id },
});
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
billid: bill.id,
successful: true,
message: JSON.stringify([t("general.labels.markedexported")]),
useremail: currentUser.email,
},
],
},
});
if (!result.errors) {
notification["success"]({
message: t("bills.successes.markexported"),
@@ -92,7 +69,11 @@ export function BillMarkExportedButton({
if (hasAccess)
return (
<Button loading={loading} disabled={bill.exported} onClick={handleUpdate}>
<Button
loading={loading}
disabled={bill.exported}
onClick={handleUpdate}
>
{t("bills.labels.markexported")}
</Button>
);

View File

@@ -1,155 +0,0 @@
import { FileAddFilled } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, notification, Tooltip } from "antd";
import { t } from "i18next";
import moment from "moment";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_INVENTORY_AND_CREDIT } from "../../graphql/inventory.queries";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
import queryString from "query-string";
import { useLocation } from "react-router-dom";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BilllineAddInventory);
export function BilllineAddInventory({
currentUser,
bodyshop,
billline,
disabled,
jobid,
}) {
const [loading, setLoading] = useState(false);
const { billid } = queryString.parse(useLocation().search);
const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
const addToInventory = async () => {
setLoading(true);
//Check to make sure there are no existing items already in the inventory.
const cm = {
vendorid: bodyshop.inhousevendorid,
invoice_number: "ih",
jobid: jobid,
isinhouse: true,
is_credit_memo: true,
date: moment().format("YYYY-MM-DD"),
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate,
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate,
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate,
total: 0,
billlines: [
{
actual_price: billline.actual_price,
actual_cost: billline.actual_cost,
quantity: billline.quantity,
line_desc: billline.line_desc,
cost_center: billline.cost_center,
deductedfromlbr: billline.deductedfromlbr,
applicable_taxes: {
local: billline.applicable_taxes.local,
state: billline.applicable_taxes.state,
federal: billline.applicable_taxes.federal,
},
},
],
};
cm.total = CalculateBillTotal(cm).enteredTotal.getAmount() / 100;
const insertResult = await insertInventoryLine({
variables: {
joblineId:
billline.joblineid === "noline" ? billline.id : billline.joblineid, //This will return null as there will be no jobline that has the id of the bill line.
//Unfortunately, we can't send null as the GQL syntax validation fails.
joblineStatus: bodyshop.md_order_statuses.default_returned,
inv: {
shopid: bodyshop.id,
billlineid: billline.id,
actual_price: billline.actual_price,
actual_cost: billline.actual_cost,
quantity: billline.quantity,
line_desc: billline.line_desc,
},
cm: { ...cm, billlines: { data: cm.billlines } }, //Fix structure for apollo insert.
pol: {
returnfrombill: billid,
vendorid: bodyshop.inhousevendorid,
deliver_by: moment().format("YYYY-MM-DD"),
parts_order_lines: {
data: [
{
line_desc: billline.line_desc,
act_price: billline.actual_price,
cost: billline.actual_cost,
quantity: billline.quantity,
job_line_id:
billline.joblineid === "noline" ? null : billline.joblineid,
part_type: billline.jobline && billline.jobline.part_type,
cm_received: true,
},
],
},
order_date: "2022-06-01",
orderedby: currentUser.email,
jobid: jobid,
user_email: currentUser.email,
return: true,
status: "Ordered",
},
},
refetchQueries: ["QUERY_BILL_BY_PK"],
});
if (!insertResult.errors) {
notification.open({
type: "success",
message: t("inventory.successes.inserted"),
});
} else {
notification.open({
type: "error",
message: t("inventory.errors.inserting", {
error: JSON.stringify(insertResult.errors),
}),
});
}
setLoading(false);
};
return (
<Tooltip title={t("inventory.actions.addtoinventory")}>
<Button
loading={loading}
disabled={
disabled || billline?.inventories?.length >= billline.quantity
}
onClick={addToInventory}
>
<FileAddFilled />
{billline?.inventories?.length > 0 && (
<div>({billline?.inventories?.length} in inv)</div>
)}
</Button>
</Tooltip>
);
}

View File

@@ -4,7 +4,6 @@ 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 { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
@@ -12,11 +11,10 @@ import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort, dateSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants";
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
//jobRO: selectJobReadOnly,
bodyshop: selectBodyshop,
});
@@ -31,7 +29,6 @@ const mapDispatchToProps = (dispatch) => ({
export function BillsListTableComponent({
bodyshop,
jobRO,
job,
billsQuery,
handleOnRowClick,
@@ -46,8 +43,6 @@ export function BillsListTableComponent({
});
// const search = queryString.parse(useLocation().search);
// const selectedBill = search.billid;
const [searchText, setSearchText] = useState("");
const Templates = TemplateList("bill");
const bills = billsQuery.data ? billsQuery.data.bills : [];
const { refetch } = billsQuery;
@@ -59,15 +54,38 @@ export function BillsListTableComponent({
</Button>
)}
<BillDeleteButton bill={record} />
<BillDetailEditReturnComponent
data={{ bills_by_pk: { ...record, jobid: job.id } }}
<Button
disabled={
record.is_credit_memo ||
record.vendorid === bodyshop.inhousevendorid ||
jobRO
record.is_credit_memo || record.vendorid === bodyshop.inhousevendorid
}
/>
onClick={() => {
console.log(record);
setPartsOrderContext({
actions: {},
context: {
jobId: job.id,
vendorId: record.vendorid,
returnFromBill: record.id,
invoiceNumber: record.invoice_number,
linesToOrder: record.billlines.map((i) => {
return {
line_desc: i.line_desc,
// db_price: i.actual_price,
act_price: i.actual_price,
cost: i.actual_cost,
quantity: i.quantity,
joblineid: i.joblineid,
oem_partno: i.jobline && i.jobline.oem_partno,
part_type: i.jobline && i.jobline.part_type,
};
}),
isReturn: true,
},
});
}}
>
{t("bills.actions.return")}
</Button>
{record.isinhouse && (
<PrintWrapperComponent
templateObject={{
@@ -149,24 +167,6 @@ export function BillsListTableComponent({
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const filteredBills = bills
? searchText === ""
? bills
: bills.filter(
(b) =>
(b.invoice_number || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(b.vendor.name || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(b.total || "")
.toString()
.toLowerCase()
.includes(searchText.toLowerCase())
)
: [];
return (
<Card
title={t("bills.labels.bills")}
@@ -207,10 +207,8 @@ export function BillsListTableComponent({
<Input.Search
placeholder={t("general.labels.search")}
value={searchText}
onChange={(e) => {
e.preventDefault();
setSearchText(e.target.value);
}}
/>
</Space>
@@ -223,7 +221,7 @@ export function BillsListTableComponent({
}}
columns={columns}
rowKey="id"
dataSource={filteredBills}
dataSource={bills}
onChange={handleTableChange}
/>
</Card>

View File

@@ -10,7 +10,7 @@ export default function CABCpvrtCalculator({ disabled, form }) {
const handleFinish = async (values) => {
logImEXEvent("job_ca_bc_pvrt_calculate");
form.setFieldsValue({ ca_bc_pvrt: ((values.rate||0) * (values.days||0)).toFixed(2) });
form.setFieldsValue({ ca_bc_pvrt: (values.rate * values.days).toFixed(2) });
setVisibility(false);
};

View File

@@ -6,13 +6,12 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
@@ -20,7 +19,6 @@ const mapDispatchToProps = (dispatch) => ({
export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector);
export function ChatMediaSelector({
bodyshop,
selectedMedia,
setSelectedMedia,
conversation,
@@ -29,6 +27,7 @@ export function ChatMediaSelector({
const [visible, setVisible] = useState(false);
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
variables: {
@@ -67,8 +66,6 @@ export function ChatMediaSelector({
</div>
);
if (bodyshop.uselocalmediaserver) return null;
return (
<Popover
content={

View File

@@ -19,7 +19,7 @@ export function ChatPresetsComponent({ bodyshop, setMessage, className }) {
const menu = (
<Menu>
{bodyshop.md_messaging_presets.map((i, idx) => (
<Menu.Item onClick={() => setMessage(i.text)} key={idx}>
<Menu.Item onClick={() => setMessage(i.text)} onItemHover key={idx}>
{i.label}
</Menu.Item>
))}

View File

@@ -279,80 +279,31 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
<Form.Item label={t("courtesycars.fields.notes")} name="notes">
<Input.TextArea />
</Form.Item>
<div>
<Form.Item
label={t("courtesycars.fields.registrationexpires")}
name="registrationexpires"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<FormDatePicker />
</Form.Item>
<Form.Item
shouldUpdate={(p, c) =>
p.registrationexpires !== c.registrationexpires
}
>
{() => {
const expires = form.getFieldValue("registrationexpires");
const dateover = expires && moment(expires).isBefore(moment());
if (dateover)
return (
<Space direction="vertical" style={{ color: "tomato" }}>
<span>
<WarningFilled style={{ marginRight: ".3rem" }} />
{t("contracts.labels.dateinpast")}
</span>
</Space>
);
return <></>;
}}
</Form.Item>
</div>
<div>
<Form.Item
label={t("courtesycars.fields.insuranceexpires")}
name="insuranceexpires"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<FormDatePicker />
</Form.Item>
<Form.Item
shouldUpdate={(p, c) =>
p.insuranceexpires !== c.insuranceexpires
}
>
{() => {
const expires = form.getFieldValue("insuranceexpires");
const dateover = expires && moment(expires).isBefore(moment());
if (dateover)
return (
<Space direction="vertical" style={{ color: "tomato" }}>
<span>
<WarningFilled style={{ marginRight: ".3rem" }} />
{t("contracts.labels.dateinpast")}
</span>
</Space>
);
return <></>;
}}
</Form.Item>
</div>
<Form.Item
label={t("courtesycars.fields.registrationexpires")}
name="registrationexpires"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<FormDatePicker />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.insuranceexpires")}
name="insuranceexpires"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<FormDatePicker />
</Form.Item>
<Form.Item label={t("courtesycars.fields.dailycost")} name="dailycost">
<CurrencyInput />
</Form.Item>

View File

@@ -34,9 +34,6 @@ const CourtesyCarStatusComponent = ({ value, onChange }, ref) => {
<Option value="courtesycars.status.sold">
{t("courtesycars.status.sold")}
</Option>
<Option value="courtesycars.status.leasereturn">
{t("courtesycars.status.leasereturn")}
</Option>
</Select>
);
};

View File

@@ -1,12 +1,11 @@
import { SyncOutlined, WarningFilled } from "@ant-design/icons";
import { Button, Card, Input, Space, Table, Tooltip } from "antd";
import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Input, Space, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import moment from "moment";
export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
const [state, setState] = useState({
sortedInfo: {},
@@ -52,33 +51,11 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
text: t("courtesycars.status.sold"),
value: "courtesycars.status.sold",
},
{
text: t("courtesycars.status.leasereturn"),
value: "courtesycars.status.leasereturn",
},
],
onFilter: (value, record) => value.includes(record.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => {
const { nextservicedate, nextservicekm, mileage } = record;
const mileageOver = nextservicekm <= mileage;
const dueForService =
nextservicedate && moment(nextservicedate).isBefore(moment());
return (
<Space>
{t(record.status)}
{(mileageOver || dueForService) && (
<Tooltip title={t("contracts.labels.cardueforservice")}>
<WarningFilled style={{ color: "tomato" }} />
</Tooltip>
)}
</Space>
);
},
render: (text, record) => t(record.status),
},
{
title: t("courtesycars.fields.year"),
@@ -128,17 +105,6 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
</Link>
) : null,
},
{
title: t("contracts.fields.scheduledreturn"),
dataIndex: "scheduledreturn",
key: "scheduledreturn",
render: (text, record) =>
record.cccontracts.length === 1 && (
<DateTimeFormatter>
{record.cccontracts[0].scheduledreturn}
</DateTimeFormatter>
),
},
];
const handleTableChange = (pagination, filters, sorter) => {

View File

@@ -1,70 +0,0 @@
import { UploadOutlined } from "@ant-design/icons";
import { Upload } from "antd";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { handleUpload } from "./documents-local-upload.utility";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
bodyshop: selectBodyshop,
});
export function DocumentsLocalUploadComponent({
children,
currentUser,
bodyshop,
job,
vendorid,
invoice_number,
callbackAfterUpload,
}) {
const [fileList, setFileList] = useState([]);
const handleDone = (uid) => {
setTimeout(() => {
setFileList((fileList) => fileList.filter((x) => x.uid !== uid));
}, 2000);
};
return (
<Upload.Dragger
multiple={true}
fileList={fileList}
onChange={(f) => {
if (f.event && f.event.percent === 100) handleDone(f.file.uid);
setFileList(f.fileList);
}}
customRequest={(ev) =>
handleUpload({
ev,
context: {
jobid: job.id,
vendorid,
invoice_number,
callback: callbackAfterUpload,
},
})
}
accept="audio/*, video/*, image/*, .pdf, .doc, .docx, .xls, .xlsx"
>
{children || (
<>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">
Click or drag files to this area to upload.
</p>
</>
)}
</Upload.Dragger>
);
}
export default connect(mapStateToProps, null)(DocumentsLocalUploadComponent);

View File

@@ -1,68 +0,0 @@
import cleanAxios from "../../utils/CleanAxios";
import { store } from "../../redux/store";
import { addMediaForJob } from "../../redux/media/media.actions";
import normalizeUrl from "normalize-url";
export const handleUpload = async ({ ev, context }) => {
const { onError, onSuccess, onProgress, file } = ev;
const { jobid, invoice_number, vendorid, callbackAfterUpload } = context;
const bodyshop = store.getState().user.bodyshop;
var options = {
headers: {
"X-Requested-With": "XMLHttpRequest",
ims_token: bodyshop.localmediatoken,
},
onUploadProgress: (e) => {
if (!!onProgress) onProgress({ percent: (e.loaded / e.total) * 100 });
},
};
const formData = new FormData();
formData.append("jobid", jobid);
if (invoice_number) {
formData.append("invoice_number", invoice_number);
formData.append("vendorid", vendorid);
}
formData.append("file", file);
const imexMediaServerResponse = await cleanAxios.post(
normalizeUrl(
`${bodyshop.localmediaserverhttp}/${
invoice_number ? "bills" : "jobs"
}/upload`
),
formData,
{
...options,
}
);
if (imexMediaServerResponse.status !== 200) {
if (!!onError) {
onError(imexMediaServerResponse.statusText);
}
} else {
onSuccess && onSuccess(file);
store.dispatch(
addMediaForJob({
jobid,
media: imexMediaServerResponse.data.map((d) => {
return {
...d,
selected: false,
src: normalizeUrl(`${bodyshop.localmediaserverhttp}/${d.src}`),
thumbnail: normalizeUrl(
`${bodyshop.localmediaserverhttp}/${d.thumbnail}`
),
};
}),
})
);
}
if (callbackAfterUpload) {
callbackAfterUpload();
}
};

View File

@@ -54,7 +54,7 @@ export const uploadToCloudinary = async (
//Set variables for getting the signed URL.
let timestamp = Math.floor(Date.now() / 1000);
let public_id = key;
let tags = `${bodyshop.imexshopid},${
let tags = `${bodyshop.textid},${
tagsArray ? tagsArray.map((tag) => `${tag},`) : ""
}`;
// let eager = process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS;

View File

@@ -23,7 +23,7 @@ export default connect(
export function EmailDocumentsComponent({
emailConfig,
form,
selectedMediaState,
}) {
const { t } = useTranslation();
@@ -45,13 +45,6 @@ export function EmailDocumentsComponent({
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
<div style={{ color: "red" }}>{t("messaging.labels.maxtenimages")}</div>
) : null}
{selectedMedia &&
selectedMedia
.filter((s) => s.isSelected)
.reduce((acc, val) => (acc = acc + val.size), 0) >=
10485760 - new Blob([form.getFieldValue("html")]).size ? (
<div style={{ color: "red" }}>{t("general.errors.sizelimit")}</div>
) : null}
{data && (
<JobDocumentsGalleryExternal
data={data ? data.documents : []}

View File

@@ -9,7 +9,6 @@ import {
Space,
Menu,
Dropdown,
Button,
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -21,13 +20,10 @@ import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { CreateExplorerLinkForJob } from "../../utils/localmedia";
import { selectEmailConfig } from "../../redux/email/email.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
emailConfig: selectEmailConfig,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
@@ -38,7 +34,6 @@ export default connect(
)(EmailOverlayComponent);
export function EmailOverlayComponent({
emailConfig,
form,
selectedMediaState,
bodyshop,
@@ -47,12 +42,7 @@ export function EmailOverlayComponent({
const { t } = useTranslation();
const handleClick = ({ item, key, keyPath }) => {
const email = item.props.value;
form.setFieldsValue({
to: _.uniq([
...form.getFieldValue("to"),
...(typeof email === "string" ? [email] : email),
]),
});
form.setFieldsValue({ to: _.uniq([...form.getFieldValue("to"), email]) });
};
const menu = (
@@ -65,11 +55,6 @@ export function EmailOverlayComponent({
{`${e.first_name} ${e.last_name}`}
</Menu.Item>
))}
{bodyshop.md_to_emails.map((e, idx) => (
<Menu.Item value={e.emails} key={idx + "group"}>
{e.label}
</Menu.Item>
))}
</Menu>
</div>
);
@@ -139,9 +124,7 @@ export function EmailOverlayComponent({
</Form.Item>
<Divider>{t("emails.labels.preview")}</Divider>
{bodyshop.attach_pdf_to_email && (
<strong>{t("emails.labels.pdfcopywillbeattached")}</strong>
)}
<strong>{t("emails.labels.pdfcopywillbeattached")}</strong>
<Form.Item shouldUpdate>
{() => {
@@ -160,20 +143,10 @@ export function EmailOverlayComponent({
</Form.Item>
<Tabs>
{!bodyshop.uselocalmediaserver && (
<Tabs.TabPane tab={t("emails.labels.documents")} key="documents">
<EmailDocumentsComponent
selectedMediaState={selectedMediaState}
form={form}
/>
</Tabs.TabPane>
)}
<Tabs.TabPane tab={t("emails.labels.documents")} key="documents">
<EmailDocumentsComponent selectedMediaState={selectedMediaState} />
</Tabs.TabPane>
<Tabs.TabPane tab={t("emails.labels.attachments")} key="attachments">
{bodyshop.uselocalmediaserver && emailConfig.jobid && (
<a href={CreateExplorerLinkForJob({ jobid: emailConfig.jobid })}>
<Button>{t("documents.labels.openinexplorer")}</Button>
</a>
)}
<Form.Item
name="fileList"
valuePropName="fileList"
@@ -183,24 +156,6 @@ export function EmailOverlayComponent({
}
return e && e.fileList;
}}
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
const totalSize = value.reduce(
(acc, val) => (acc = acc + val.size),
0
);
const limit =
10485760 - new Blob([form.getFieldValue("html")]).size;
if (totalSize > limit) {
return Promise.reject(t("general.errors.sizelimit"));
}
return Promise.resolve();
},
}),
]}
>
<Upload.Dragger
beforeUpload={Upload.LIST_IGNORE}

View File

@@ -168,6 +168,7 @@ export function EmailOverlayContainer({
useEffect(() => {
if (modalVisible) render();
}, [modalVisible]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<Modal
destroyOnClose={true}
@@ -177,15 +178,7 @@ export function EmailOverlayContainer({
onCancel={() => {
toggleEmailOverlayVisible();
}}
okButtonProps={{
loading: sending,
disabled:
selectedMedia &&
( (selectedMedia
.filter((s) => s.isSelected)
.reduce((acc, val) => (acc = acc + val.size), 0) >=
10485760 - new Blob([form.getFieldValue("html")]).size) || selectedMedia.filter((s) => s.isSelected).length > 10),
}}
okButtonProps={{ loading: sending }}
>
<Form layout="vertical" form={form} onFinish={handleFinish}>
{loading && (

View File

@@ -30,7 +30,6 @@ class ErrorBoundary extends React.Component {
static getDerivedStateFromError(error) {
console.log("ErrorBoundary -> getDerivedStateFromError -> error", error);
return { hasErrored: true, error: error };
}

View File

@@ -1,25 +0,0 @@
import React from "react";
import { WarningOutlined } from "@ant-design/icons";
import { Space, Tooltip } from "antd";
import { useTranslation } from "react-i18next";
const style = {
fontWeight: "bold",
color: "green",
};
export default function ExportLogsCountDisplay({ logs }) {
const success = logs.filter((e) => e.successful).length;
const attempts = logs.length;
const { t } = useTranslation();
return (
<Space style={success > 0 ? style : {}}>
{`${success}/${attempts}`}
{success > 0 && (
<Tooltip title={t("exportlogs.labels.priorsuccesfulexport")}>
<WarningOutlined />
</Tooltip>
)}
</Space>
);
}

View File

@@ -5,7 +5,7 @@ import AlertComponent from "../alert/alert.component";
import { Prompt, useLocation } from "react-router-dom";
import "./form-fields-changed.styles.scss";
export default function FormsFieldChanged({ form, skipPrompt }) {
export default function FormsFieldChanged({ form }) {
const { t } = useTranslation();
const handleReset = () => {
@@ -25,7 +25,7 @@ export default function FormsFieldChanged({ form, skipPrompt }) {
return (
<Space direction="vertical" style={{ width: "100%" }}>
<Prompt
when={skipPrompt ? false : true}
when={true}
message={(location) => {
if (loc.pathname === location.pathname) return false;
return t("general.messages.unsavedchangespopup");

View File

@@ -11,7 +11,6 @@ import AlertComponent from "../alert/alert.component";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
export default function GlobalSearch() {
const { t } = useTranslation();
const history = useHistory();
@@ -98,11 +97,7 @@ export default function GlobalSearch() {
} ${vehicle.v_model_desc || ""}`}
</span>
<span>{vehicle.plate_no || ""}</span>
<span>
<VehicleVinDisplay>
{vehicle.v_vin || ""}
</VehicleVinDisplay>
</span>
<span> {vehicle.v_vin || ""}</span>
</Space>
</Link>
),

View File

@@ -1,10 +1,8 @@
import { useTreatments } from "@splitsoftware/splitio-react";
import Icon, {
BankFilled,
BarChartOutlined,
CarFilled,
ClockCircleFilled,
CheckCircleOutlined,
DashboardFilled,
DollarCircleFilled,
ExportOutlined,
@@ -84,12 +82,6 @@ function Header({
setReportCenterContext,
recentItems,
}) {
const { Simple_Inventory } = useTreatments(
["Simple_Inventory"],
{},
bodyshop && bodyshop.imexshopid
);
const { t } = useTranslation();
return (
@@ -116,9 +108,6 @@ function Header({
<Menu.Item key="activejobs" icon={<FileFilled />}>
<Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
</Menu.Item>
<Menu.Item key="readyjobs" icon={<CheckCircleOutlined />}>
<Link to="/manage/jobs/ready">{t("menus.header.readyjobs")}</Link>
</Menu.Item>
<Menu.Item key="parts-queue" icon={<ToolFilled />}>
<Link to="/manage/partsqueue">{t("menus.header.parts-queue")}</Link>
</Menu.Item>
@@ -206,20 +195,7 @@ function Header({
>
{t("menus.header.enterbills")}
</Menu.Item>
{Simple_Inventory.treatment === "on" && (
<>
<Menu.Divider key="div4" />
<Menu.Item
key="inventory"
icon={<Icon component={FaFileInvoiceDollar} />}
>
<Link to="/manage/inventory">
{t("menus.header.inventory")}
</Link>
</Menu.Item>
</>
)}
<Menu.Divider key="div7" />
<Menu.Divider key="div4" />
<Menu.Item key="allpayments" icon={<BankFilled />}>
<Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
</Menu.Item>
@@ -236,6 +212,7 @@ function Header({
{t("menus.header.enterpayment")}
</Menu.Item>
<Menu.Divider key="div5" />
<Menu.Item key="timetickets" icon={<FieldTimeOutlined />}>
<Link to="/manage/timetickets">
{t("menus.header.timetickets")}
@@ -254,6 +231,7 @@ function Header({
{t("menus.header.entertimeticket")}
</Menu.Item>
<Menu.Divider key="div6" />
<Menu.SubMenu
key="accountingexport"
title={t("menus.header.export")}

View File

@@ -1,65 +0,0 @@
import { Button } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import moment from "moment";
import { useTranslation } from "react-i18next";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setBillEnterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "billEnter" })),
});
export default connect(mapStateToProps, mapDispatchToProps)(InventoryBillRo);
export function InventoryBillRo({
bodyshop,
setBillEnterContext,
inventoryline,
}) {
const { t } = useTranslation();
return (
<Button
onClick={() => {
setBillEnterContext({
actions: {
//refetch: refetch
},
context: {
disableInvNumber: true,
//job: { id: job.id },
consumeinventoryid: inventoryline.id,
bill: {
vendorid: bodyshop.inhousevendorid,
invoice_number: "ih",
isinhouse: true,
date: moment(),
total: 0,
billlines: [{}],
// billlines: selectedLines.map((p) => {
// return {
// joblineid: p.id,
// actual_price: p.act_price,
// actual_cost: 0, //p.act_price,
// line_desc: p.line_desc,
// line_remarks: p.line_remarks,
// part_type: p.part_type,
// quantity: p.quantity || 1,
// applicable_taxes: {
// local: false,
// state: false,
// federal: false,
// },
// };
// }),
},
},
});
}}
>
{t("inventory.actions.addtoro")}
</Button>
);
}

View File

@@ -1,67 +0,0 @@
import { DeleteFilled } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, notification, Popconfirm } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { DELETE_INVENTORY_LINE } from "../../graphql/inventory.queries";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
export default function InventoryLineDelete({
inventoryline,
disabled,
refetch,
}) {
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
const [deleteInventoryLine] = useMutation(DELETE_INVENTORY_LINE);
const handleDelete = async () => {
setLoading(true);
const result = await deleteInventoryLine({
variables: { lineId: inventoryline.id },
// update(cache, { errors }) {
// cache.modify({
// fields: {
// inventory(existingInventory, { readField }) {
// console.log(existingInventory);
// return existingInventory.filter(
// (invRef) => inventoryline.id !== readField("id", invRef)
// );
// },
// },
// });
// },
});
if (!!!result.errors) {
notification["success"]({ message: t("inventory.successes.deleted") });
} else {
//Check if it's an fkey violation.
notification["error"]({
message: t("bills.errors.deleting", {
error: JSON.stringify(result.errors),
}),
});
}
if (refetch) refetch();
setLoading(false);
};
return (
<RbacWrapper action="inventory:delete" noauth={<></>}>
<Popconfirm
disabled={disabled || inventoryline.consumedbybillid}
onConfirm={handleDelete}
title={t("inventory.labels.deleteconfirm")}
>
<Button
disabled={disabled || inventoryline.consumedbybillid}
loading={loading}
>
<DeleteFilled />
</Button>
</Popconfirm>
</RbacWrapper>
);
}

View File

@@ -1,228 +0,0 @@
import { EditFilled, SyncOutlined, FileAddFilled } from "@ant-design/icons";
import { Button, Card, Input, Space, Table, Typography } from "antd";
import queryString from "query-string";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import InventoryBillRo from "../inventory-bill-ro/inventory-bill-ro.component";
import InventoryLineDelete from "../inventory-line-delete/inventory-line-delete.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setInventoryUpsertContext: (context) =>
dispatch(setModalContext({ context: context, modal: "inventoryUpsert" })),
});
export function JobsList({
bodyshop,
refetch,
loading,
jobs,
total,
setInventoryUpsertContext,
}) {
const search = queryString.parse(useLocation().search);
const { page, sortcolumn, sortorder } = search;
const history = useHistory();
const { t } = useTranslation();
const columns = [
{
title: t("billlines.fields.line_desc"),
dataIndex: "line_desc",
key: "line_desc",
sorter: true, //(a, b) => alphaSort(a.line_desc, b.line_desc),
sortOrder: sortcolumn === "line_desc" && sortorder,
render: (text, record) =>
record.billline?.bill?.job ? (
<div>
<div>{text}</div>
<strong>{`(${record.billline?.bill?.job?.v_model_yr} ${record.billline?.bill?.job?.v_make_desc} ${record.billline?.bill?.job?.v_model_desc})`}</strong>
</div>
) : (
text
),
},
{
title: t("inventory.labels.frombillinvoicenumber"),
dataIndex: "vendorname",
key: "vendorname",
ellipsis: true,
//sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
//sortOrder: sortcolumn === "ownr_ln" && sortorder,
render: (text, record) =>
(
(record.billline?.bill?.invoice_number || "") +
" " +
(record.manualinvoicenumber || "")
).trim(),
},
{
title: t("inventory.labels.fromvendor"),
dataIndex: "vendorname",
key: "vendorname",
ellipsis: true,
//sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
//sortOrder: sortcolumn === "ownr_ln" && sortorder,
render: (text, record) =>
(
(record.billline?.bill?.vendor?.name || "") +
" " +
(record.manualvendor || "")
).trim(),
},
{
title: t("billlines.fields.actual_price"),
dataIndex: "actual_price",
key: "actual_price",
render: (text, record) => (
<CurrencyFormatter>{record.actual_price}</CurrencyFormatter>
),
},
{
title: t("billlines.fields.actual_cost"),
dataIndex: "actual_cost",
key: "actual_cost",
render: (text, record) => (
<CurrencyFormatter>{record.actual_cost}</CurrencyFormatter>
),
},
{
title: t("inventory.fields.comment"),
dataIndex: "comment",
key: "comment",
},
{
title: t("inventory.labels.consumedbyjob"),
dataIndex: "consumedbyjob",
key: "consumedbyjob",
ellipsis: true,
render: (text, record) =>
record.bill?.job?.ro_number ? (
<Link to={`/manage/jobs/${record.bill?.job?.id}`}>
{record.bill?.job?.ro_number}
</Link>
) : (
<InventoryBillRo inventoryline={record} />
),
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
ellipsis: true,
render: (text, record) => (
<Space wrap>
<Button
onClick={() => {
setInventoryUpsertContext({
actions: { refetch: refetch },
context: {
existingInventory: record,
},
});
}}
>
<EditFilled />
</Button>
<InventoryLineDelete inventoryline={record} refetch={refetch} />
</Space>
),
},
];
const handleTableChange = (pagination, filters, sorter) => {
search.page = pagination.current;
search.sortcolumn = sorter.column && sorter.column.key;
search.sortorder = sorter.order;
history.push({ search: queryString.stringify(search) });
};
return (
<Card
extra={
<Space wrap>
{search.search && (
<>
<Typography.Title level={4}>
{t("general.labels.searchresults", { search: search.search })}
</Typography.Title>
<Button
onClick={() => {
delete search.search;
history.push({ search: queryString.stringify(search) });
}}
>
{t("general.actions.clear")}
</Button>
</>
)}
<Button
onClick={() => {
setInventoryUpsertContext({
actions: { refetch: refetch },
context: {},
});
}}
>
<FileAddFilled />
</Button>
<Button
onClick={() => {
if (search.showall) delete search.showall;
else {
search.showall = true;
}
history.push({ search: queryString.stringify(search) });
}}
>
{search.showall
? t("inventory.labels.showavailable")
: t("inventory.labels.showall")}
</Button>
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<Input.Search
placeholder={search.search || t("general.labels.search")}
onSearch={(value) => {
search.search = value;
history.push({ search: queryString.stringify(search) });
}}
enterButton
/>
</Space>
}
>
<Table
loading={loading}
pagination={{
position: "top",
pageSize: 25,
current: parseInt(page || 1),
total: total,
}}
columns={columns}
rowKey="id"
dataSource={jobs}
onChange={handleTableChange}
/>
</Card>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(JobsList);

View File

@@ -1,64 +0,0 @@
import { useQuery } from "@apollo/client";
import queryString from "query-string";
import React from "react";
import { connect } from "react-redux";
import { useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { QUERY_INVENTORY_PAGINATED } from "../../graphql/inventory.queries";
import {
setBreadcrumbs,
setSelectedHeader,
} from "../../redux/application/application.actions";
import AlertComponent from "../alert/alert.component";
import InventoryListPaginated from "./inventory-list.component";
const mapStateToProps = createStructuredSelector({
//bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
});
export function InventoryList({ setBreadcrumbs, setSelectedHeader }) {
const searchParams = queryString.parse(useLocation().search);
const { page, sortcolumn, sortorder, search, showall } = searchParams;
const { loading, error, data, refetch } = useQuery(
QUERY_INVENTORY_PAGINATED,
{
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
variables: {
search: search || "",
offset: page ? (page - 1) * 25 : 0,
limit: 25,
consumedIsNull: showall === "true" ? null : true,
order: [
{
[sortcolumn || "created_at"]:
sortorder && sortorder !== "false"
? sortorder === "descend"
? "desc"
: "asc"
: "desc",
},
],
},
}
);
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<InventoryListPaginated
refetch={refetch}
loading={loading}
searchParams={searchParams}
total={data ? data.search_inventory_aggregate.aggregate.count : 0}
jobs={data ? data.search_inventory : []}
/>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(InventoryList);

View File

@@ -1,68 +0,0 @@
import { Form, Input, Space } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectInventoryUpsert } from "../../redux/modals/modals.selectors";
import FormItemCurrency from "../form-items-formatted/currency-form-item.component";
const mapStateToProps = createStructuredSelector({
inventoryUpsertModal: selectInventoryUpsert,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(NoteUpsertModalComponent);
export function NoteUpsertModalComponent({ form, inventoryUpsertModal }) {
const { t } = useTranslation();
const { existingInventory } = inventoryUpsertModal.context;
return (
<Space wrap>
<Form.Item
label={t("billlines.fields.line_desc")}
rules={[{ required: true }]}
name="line_desc"
>
<Input />
</Form.Item>
<Form.Item label={t("inventory.fields.comment")} name="comment">
<Input />
</Form.Item>
{!existingInventory && (
<>
<Form.Item
label={t("inventory.fields.manualinvoicenumber")}
name="manualinvoicenumber"
>
<Input />
</Form.Item>
<Form.Item
label={t("inventory.fields.manualvendor")}
name="manualvendor"
>
<Input />
</Form.Item>
<Form.Item
rules={[{ required: true }]}
label={t("billlines.fields.actual_cost")}
name="actual_cost"
>
<FormItemCurrency />
</Form.Item>
<Form.Item
rules={[{ required: true }]}
label={t("billlines.fields.actual_price")}
name="actual_price"
>
<FormItemCurrency />
</Form.Item>
</>
)}
</Space>
);
}

View File

@@ -1,126 +0,0 @@
import { useMutation } from "@apollo/client";
import { Form, Modal, notification } from "antd";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import {
INSERT_INVENTORY_LINE,
UPDATE_INVENTORY_LINE,
} from "../../graphql/inventory.queries";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectInventoryUpsert } from "../../redux/modals/modals.selectors";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import InventoryUpsertModal from "./inventory-upsert-modal.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
inventoryUpsertModal: selectInventoryUpsert,
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("inventoryUpsert")),
});
export function InventoryUpsertModalContainer({
currentUser,
bodyshop,
inventoryUpsertModal,
toggleModalVisible,
}) {
const { t } = useTranslation();
const [insertInventory] = useMutation(INSERT_INVENTORY_LINE);
const [updateInventoryLine] = useMutation(UPDATE_INVENTORY_LINE);
const { visible, context, actions } = inventoryUpsertModal;
const { existingInventory } = context;
const { refetch } = actions;
const [form] = Form.useForm();
useEffect(() => {
//Required to prevent infinite looping.
if (existingInventory && visible) {
form.setFieldsValue(existingInventory);
} else if (!existingInventory && visible) {
form.resetFields();
}
}, [existingInventory, form, visible]);
const handleFinish = async (formValues) => {
const values = formValues;
if (existingInventory) {
logImEXEvent("inventory_update");
updateInventoryLine({
variables: {
inventoryId: existingInventory.id,
inventoryItem: values,
},
}).then((r) => {
notification["success"]({
message: t("inventory.successes.updated"),
});
});
// if (refetch) refetch();
toggleModalVisible();
} else {
logImEXEvent("inventory_insert");
await insertInventory({
variables: {
inventoryItem: { shopid: bodyshop.id, ...values },
},
update(cache, { data }) {
cache.modify({
fields: {
inventory(existingInv) {
return [...existingInv, data.insert_inventory_one];
},
},
});
},
});
if (refetch) refetch();
form.resetFields();
toggleModalVisible();
notification["success"]({
message: t("inventory.successes.inserted"),
});
}
};
return (
<Modal
title={
existingInventory
? t("inventory.actions.edit")
: t("inventory.actions.new")
}
visible={visible}
okText={t("general.actions.save")}
onOk={() => {
form.submit();
}}
onCancel={() => {
toggleModalVisible();
}}
destroyOnClose
>
<Form form={form} onFinish={handleFinish} layout="vertical">
<InventoryUpsertModal form={form} />
</Form>
</Modal>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(InventoryUpsertModalContainer);

View File

@@ -49,11 +49,11 @@ export function Jobd3RdPartyModal({ bodyshop, jobId }) {
GenerateDocument(
{
name: TemplateList("job_special").special_thirdpartypayer.key,
name: TemplateList("job_special").thirdpartypayer.key,
variables: { id: jobId },
context: restVals,
},
{ subject: TemplateList("job_special").special_thirdpartypayer.subject },
{ subject: TemplateList("job_special").thirdpartypayer.subject },
sendtype
);
};

View File

@@ -247,40 +247,40 @@ export function ScheduleEventComponent({
);
const RegularEvent = event.isintake ? (
<Space
wrap
size='small'
<div
style={{
backgroundColor:
event.color && event.color.hex ? event.color.hex : event.color,
display: "flex",
flexWrap: "wrap",
height: "100%",
}}
>
{event.note && <AlertFilled className="production-alert" />}
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
<OwnerNameDisplay ownerObject={event.job} />
{`${(event.job && event.job.v_model_yr) || ""} ${
(event.job && event.job.v_make_desc) || ""
} ${(event.job && event.job.v_model_desc) || ""}`}
{`(${(event.job && event.job.labhrs.aggregate.sum.mod_lb_hrs) || "0"} / ${
(event.job && event.job.larhrs.aggregate.sum.mod_lb_hrs) || "0"
})`}
<Space>
{event.note && <AlertFilled className="production-alert" />}
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
<span>
<OwnerNameDisplay ownerObject={event.job} />
</span>
</Space>
<Space>
<span>
{`${(event.job && event.job.v_model_yr) || ""} ${
(event.job && event.job.v_make_desc) || ""
} ${(event.job && event.job.v_model_desc) || ""}`}
</span>
<span>
{`(${
(event.job && event.job.labhrs.aggregate.sum.mod_lb_hrs) || "0"
} / ${
(event.job && event.job.larhrs.aggregate.sum.mod_lb_hrs) || "0"
})`}
</span>
</Space>
{event.job && event.job.alt_transport && (
<div style={{ margin: ".1rem" }}>{event.job.alt_transport}</div>
)}
</Space>
</div>
) : (
<div
style={{
height: "100%",
width: "100%",
backgroundColor:
event.color && event.color.hex ? event.color.hex : event.color,
}}
>
<div style={{ height: "100%", width: "100%" }}>
<strong>{`${event.title || ""}`}</strong>
</div>
);
@@ -291,13 +291,7 @@ export function ScheduleEventComponent({
onVisibleChange={(vis) => !event.vacation && setVisible(vis)}
trigger="click"
content={event.block ? blockContent : popoverContent}
style={{
height: "100%",
width: "100%",
backgroundColor:
event.color && event.color.hex ? event.color.hex : event.color,
}}
style={{ height: "100%", width: "100%" }}
>
{RegularEvent}
</Popover>

View File

@@ -26,8 +26,6 @@ export default function JobBillsTotalComponent({
let billCms = Dinero();
let lbrAdjustments = Dinero();
let totalReturns = Dinero();
let totalReturnsMarkedNotReceived = Dinero();
let totalReturnsMarkedReceived = Dinero();
partsOrders.forEach((p) =>
p.parts_order_lines.forEach((pol) => {
@@ -37,24 +35,6 @@ export default function JobBillsTotalComponent({
amount: Math.round((pol.act_price || 0) * 100),
}).multiply(pol.quantity)
);
if (pol.cm_received === null) {
return; // Skip this calculation for bills posted prior to the CNR change.
} else {
if (pol.cm_received === false) {
totalReturnsMarkedNotReceived = totalReturnsMarkedNotReceived.add(
Dinero({
amount: Math.round((pol.act_price || 0) * 100),
}).multiply(pol.quantity)
);
} else {
totalReturnsMarkedReceived = totalReturnsMarkedReceived.add(
Dinero({
amount: Math.round((pol.act_price || 0) * 100),
}).multiply(pol.quantity)
);
}
}
}
})
);
@@ -86,7 +66,6 @@ export default function JobBillsTotalComponent({
const totalPartsSublet = Dinero(totals.parts.parts.total)
.add(Dinero(totals.parts.sublets.total))
.add(Dinero(totals.additional.shipping))
.add(Dinero(totals.additional.towing));
const discrepancy = totalPartsSublet.subtract(billTotals);
@@ -94,7 +73,7 @@ export default function JobBillsTotalComponent({
const discrepWithLbrAdj = discrepancy.add(lbrAdjustments);
const discrepWithCms = discrepWithLbrAdj.add(totalReturns);
const calculatedCreditsNotReceived = totalReturns.subtract(billCms); //billCms is tracked as a negative number.
const creditsNotReceived = totalReturns.subtract(billCms); //billCms is tracked as a negative number.
return (
<Row gutter={16}>
@@ -234,32 +213,6 @@ export default function JobBillsTotalComponent({
value={totalReturns.toFormat()}
/>
</Tooltip>
<Tooltip
title={
<div
dangerouslySetInnerHTML={{
__html: t(
"jobs.labels.plitooltips.calculatedcreditsnotreceived"
),
}}
/>
}
>
<Statistic
title={t("bills.labels.calculatedcreditsnotreceived")}
valueStyle={{
color:
calculatedCreditsNotReceived.getAmount() <= 0
? "green"
: "red",
}}
value={
calculatedCreditsNotReceived.getAmount() >= 0
? calculatedCreditsNotReceived.toFormat()
: Dinero().toFormat()
}
/>
</Tooltip>
<Tooltip
title={
<div
@@ -272,14 +225,11 @@ export default function JobBillsTotalComponent({
<Statistic
title={t("bills.labels.creditsnotreceived")}
valueStyle={{
color:
totalReturnsMarkedNotReceived.getAmount() <= 0
? "green"
: "red",
color: creditsNotReceived.getAmount() <= 0 ? "green" : "red",
}}
value={
totalReturnsMarkedNotReceived.getAmount() >= 0
? totalReturnsMarkedNotReceived.toFormat()
creditsNotReceived.getAmount() >= 0
? creditsNotReceived.toFormat()
: Dinero().toFormat()
}
/>

View File

@@ -6,10 +6,8 @@ import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { QUERY_JOB_CARD_DETAILS } from "../../graphql/jobs.queries";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import JobSyncButton from "../job-sync-button/job-sync-button.component";
import JobsDetailHeader from "../jobs-detail-header/jobs-detail-header.component";
@@ -22,10 +20,6 @@ import JobDetailCardsNotesComponent from "./job-detail-cards.notes.component";
import JobDetailCardsPartsComponent from "./job-detail-cards.parts.component";
import JobDetailCardsTotalsComponent from "./job-detail-cards.totals.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setPrintCenterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "printCenter" })),
@@ -37,7 +31,7 @@ const span = {
lg: { span: 8 },
};
export function JobDetailCards({ bodyshop, setPrintCenterContext }) {
export function JobDetailCards({ setPrintCenterContext }) {
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
@@ -149,14 +143,12 @@ export function JobDetailCards({ bodyshop, setPrintCenterContext }) {
data={data ? data.jobs_by_pk : null}
/>
</Col>
{!bodyshop.uselocalmediaserver && (
<Col {...span}>
<JobDetailCardsDocumentsComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Col>
)}
<Col {...span}>
<JobDetailCardsDocumentsComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Col>
<Col {...span}>
<JobDetailCardsDamageComponent
loading={loading}
@@ -169,4 +161,4 @@ export function JobDetailCards({ bodyshop, setPrintCenterContext }) {
</Drawer>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(JobDetailCards);
export default connect(null, mapDispatchToProps)(JobDetailCards);

View File

@@ -6,7 +6,6 @@ import {
EditFilled,
PlusCircleTwoTone,
MinusCircleTwoTone,
HomeOutlined,
} from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import {
@@ -43,8 +42,6 @@ import _ from "lodash";
import JobCreateIOU from "../job-create-iou/job-create-iou.component";
import JobLinesExpander from "./job-lines-expander.component";
import { selectBodyshop } from "../../redux/user/user.selectors";
import moment from "moment";
import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -57,8 +54,6 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setModalContext({ context: context, modal: "jobLineEdit" })),
setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
setBillEnterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "billEnter" })),
});
export function JobLinesComponent({
@@ -73,7 +68,6 @@ export function JobLinesComponent({
job,
setJobLineEditContext,
form,
setBillEnterContext,
}) {
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
@@ -115,10 +109,7 @@ 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(),
render: (text, record) => record.oem_partno,
},
{
title: t("joblines.fields.op_code_desc"),
@@ -144,26 +135,11 @@ export function JobLinesComponent({
filters: [
{
text: t("jobs.labels.partsfilter"),
value: [
"PAN",
"PAC",
"PAR",
"PAL",
"PAA",
"PAM",
"PAP",
"PAS",
"PASL",
"PAG",
],
value: ["PAN", "PAP", "PAL", "PAA", "PAS", "PASL"],
},
{
text: t("joblines.fields.part_types.PAN"),
value: ["PAN"],
},
{
text: t("joblines.fields.part_types.PAP"),
value: ["PAP"],
value: ["PAN", "PAP"],
},
{
text: t("joblines.fields.part_types.PAL"),
@@ -173,29 +149,9 @@ export function JobLinesComponent({
text: t("joblines.fields.part_types.PAA"),
value: ["PAA"],
},
{
text: t("joblines.fields.part_types.PAG"),
value: ["PAG"],
},
{
text: t("joblines.fields.part_types.PAS"),
value: ["PAS"],
},
{
text: t("joblines.fields.part_types.PASL"),
value: ["PASL"],
},
{
text: t("joblines.fields.part_types.PAC"),
value: ["PAC"],
},
{
text: t("joblines.fields.part_types.PAR"),
value: ["PAR"],
},
{
text: t("joblines.fields.part_types.PAM"),
value: ["PAM"],
value: ["PAS", "PASL"],
},
],
onFilter: (value, record) => value.includes(record.part_type),
@@ -214,7 +170,7 @@ export function JobLinesComponent({
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
ellipsis: true,
render: (text, record) => (
<JobLineConvertToLabor jobline={record} job={job}>
<>
<CurrencyFormatter>
{record.db_ref === "900510" || record.db_ref === "900511"
? record.prt_dsmk_m
@@ -227,7 +183,7 @@ export function JobLinesComponent({
) : (
<></>
)}
</JobLineConvertToLabor>
</>
),
},
{
@@ -334,9 +290,9 @@ export function JobLinesComponent({
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<Space>
<div>
{(record.manual_line || jobIsPrivate) && (
<>
<Space>
<Button
disabled={jobRO}
onClick={() => {
@@ -373,19 +329,15 @@ export function JobLinesComponent({
>
<DeleteFilled />
</Button>
</>
</Space>
)}
</Space>
</div>
),
},
];
const handleTableChange = (pagination, filters, sorter) => {
setState((state) => ({
...state,
filteredInfo: filters,
sortedInfo: sorter,
}));
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const handleMark = (e) => {
@@ -434,62 +386,6 @@ export function JobLinesComponent({
</Space>
</Tag>
)}
<Button
disabled={
(job && !job.converted) ||
(selectedLines.length > 0 ? false : true) ||
jobRO ||
technician
}
onClick={() => {
// setPartsOrderContext({
// actions: { refetch: refetch },
// context: {
// jobId: job.id,
// job: job,
// linesToOrder: selectedLines,
// },
// });
setBillEnterContext({
actions: { refetch: refetch },
context: {
disableInvNumber: true,
job: { id: job.id },
bill: {
vendorid: bodyshop.inhousevendorid,
invoice_number: "ih",
isinhouse: true,
date: new moment(),
total: 0,
billlines: selectedLines.map((p) => {
return {
joblineid: p.id,
actual_price: p.act_price,
actual_cost: 0, //p.act_price,
line_desc: p.line_desc,
line_remarks: p.line_remarks,
part_type: p.part_type,
quantity: p.quantity || 1,
applicable_taxes: {
local: false,
state: false,
federal: false,
},
};
}),
},
},
});
//Clear out the selected lines. IO-785
setSelectedLines([]);
}}
>
<HomeOutlined />
{t("parts.actions.orderinhouse")}
{selectedLines.length > 0 && ` (${selectedLines.length})`}
</Button>
<Button
disabled={
(job && !job.converted) ||
@@ -503,12 +399,7 @@ export function JobLinesComponent({
context: {
jobId: job.id,
job: job,
linesToOrder: selectedLines.map((l) => ({
...l,
oem_partno: `${l.oem_partno || ""} ${
l.alt_partno ? `(${l.alt_partno})` : ""
}`.trim(),
})),
linesToOrder: selectedLines,
},
});
@@ -521,24 +412,12 @@ export function JobLinesComponent({
</Button>
<Button
onClick={() => {
setState((state) => ({
setState({
...state,
filteredInfo: {
...state.filteredInfo,
part_type: [
"PAN",
"PAC",
"PAR",
"PAL",
"PAA",
"PAM",
"PAP",
"PAS",
"PASL",
"PAG",
],
part_type: ["PAN,PAC,PAR,PAL,PAA,PAM,PAP,PAS,PASL,PAG"],
},
}));
});
}}
>
<FilterFilled /> {t("jobs.actions.filterpartsonly")}

View File

@@ -1,256 +0,0 @@
import { ClockCircleOutlined } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
import {
Button,
Card,
Form,
notification,
Popover,
Select,
Space,
Tooltip,
} from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import {
QUERY_JOB_LBR_ADJUSTMENTS,
UPDATE_JOB,
} from "../../graphql/jobs.queries";
import _ from "lodash";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobLineConvertToLabor);
export function JobLineConvertToLabor({
children,
jobline,
job,
insertAuditTrail,
...otherBtnProps
}) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
const [visibility, setVisibility] = useState(false);
const client = useApolloClient();
const handleFinish = async (values) => {
const { mod_lbr_ty } = values;
logImEXEvent("job_convert_dollar_to_labor");
setLoading(true);
const existingAdjustments = await client.query({
query: QUERY_JOB_LBR_ADJUSTMENTS,
variables: {
id: job.id,
},
});
const newAdjustments = _.cloneDeep(
existingAdjustments.data.jobs_by_pk.lbr_adjustments
);
const adjustment = calculateAdjustment({ mod_lbr_ty, job, jobline });
newAdjustments[mod_lbr_ty] = (newAdjustments[mod_lbr_ty] || 0) + adjustment;
const jobUpdate = client.mutate({
mutation: UPDATE_JOB,
variables: {
jobId: job.id,
job: { lbr_adjustments: newAdjustments },
},
});
const lineUpdate = client.mutate({
mutation: UPDATE_JOB_LINE,
variables: {
lineId: jobline.id,
line: {
convertedtolbr: true,
convertedtolbr_data: {
mod_lbr_ty: mod_lbr_ty,
mod_lb_hrs: adjustment,
},
},
},
});
if (!!jobUpdate.errors) {
notification["error"]({
message: t("jobs.errors.saving", {
message: JSON.stringify(jobUpdate.errors),
}),
});
return;
}
if (!!lineUpdate.errors) {
notification["error"]({
message: t("joblines.errors.saving", {
message: JSON.stringify(lineUpdate.errors),
}),
});
return;
}
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobmodifylbradj({
hours: calculateAdjustment({ mod_lbr_ty, job, jobline }).toFixed(1),
mod_lbr_ty,
}),
});
setLoading(false);
setVisibility(false);
};
const overlay = (
<Card>
<Form form={form} layout="vertical" onFinish={handleFinish}>
<Form.Item
label={t("joblines.fields.mod_lbr_ty")}
name="mod_lbr_ty"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select allowClear optionFilterProp="children" showSearch>
<Select.Option value="LAA">
{t("joblines.fields.lbr_types.LAA")}
</Select.Option>
<Select.Option value="LAB">
{t("joblines.fields.lbr_types.LAB")}
</Select.Option>
<Select.Option value="LAD">
{t("joblines.fields.lbr_types.LAD")}
</Select.Option>
<Select.Option value="LAE">
{t("joblines.fields.lbr_types.LAE")}
</Select.Option>
<Select.Option value="LAF">
{t("joblines.fields.lbr_types.LAF")}
</Select.Option>
<Select.Option value="LAG">
{t("joblines.fields.lbr_types.LAG")}
</Select.Option>
<Select.Option value="LAM">
{t("joblines.fields.lbr_types.LAM")}
</Select.Option>
<Select.Option value="LAR">
{t("joblines.fields.lbr_types.LAR")}
</Select.Option>
<Select.Option value="LAS">
{t("joblines.fields.lbr_types.LAS")}
</Select.Option>
<Select.Option value="LAU">
{t("joblines.fields.lbr_types.LAU")}
</Select.Option>
<Select.Option value="LA1">
{t("joblines.fields.lbr_types.LA1")}
</Select.Option>
<Select.Option value="LA2">
{t("joblines.fields.lbr_types.LA2")}
</Select.Option>
<Select.Option value="LA3">
{t("joblines.fields.lbr_types.LA3")}
</Select.Option>
<Select.Option value="LA4">
{t("joblines.fields.lbr_types.LA4")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item shouldUpdate>
{() => {
const { mod_lbr_ty } = form.getFieldsValue();
return t("joblines.labels.adjustmenttobeadded", {
adjustment: calculateAdjustment({
mod_lbr_ty,
job,
jobline,
}).toFixed(1),
});
}}
</Form.Item>
<Space wrap>
<Button
type="primary"
disabled={jobline.convertedtolbr}
htmlType="submit"
>
{t("general.actions.save")}
</Button>
<Button onClick={() => setVisibility(false)}>
{t("general.actions.cancel")}
</Button>
</Space>
</Form>
</Card>
);
const handleClick = (e) => {
setLoading(true);
form.setFieldsValue({
// date: new moment(),
// bodyhrs: Math.round(v.bodyhrs * 10) / 10,
// painthrs: Math.round(v.painthrs * 10) / 10,
});
setVisibility(true);
setLoading(false);
};
return (
<>
{children}
{jobline.act_price !== 0 && (
<Popover
disabled={jobline.convertedtolbr}
content={overlay}
visible={visibility}
placement="bottom"
>
<Tooltip title={t("joblines.actions.converttolabor")}>
<Button
type="link"
disabled={jobline.convertedtolbr}
loading={loading}
onClick={handleClick}
{...otherBtnProps}
>
<ClockCircleOutlined />
</Button>
</Tooltip>
</Popover>
)}
</>
);
}
function calculateAdjustment({ mod_lbr_ty, job, jobline }) {
if (!mod_lbr_ty) return 0;
const rate = job[`rate_${mod_lbr_ty.toLowerCase()}`];
if (rate === 0 || rate === null || rate === undefined) return 0;
const adj = jobline.act_price / job[`rate_${mod_lbr_ty.toLowerCase()}`];
return adj;
}

View File

@@ -24,7 +24,7 @@ export function JoblinePresetButton({ bodyshop, form }) {
const menu = (
<Menu>
{bodyshop.md_jobline_presets.map((i, idx) => (
<Menu.Item onClick={() => handleSelect(i)} key={idx}>
<Menu.Item onClick={() => handleSelect(i)} onItemHover key={idx}>
{i.label}
</Menu.Item>
))}

View File

@@ -141,9 +141,7 @@ export function JobLinesUpsertModalComponent({
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
if (
!!getFieldValue("mod_lbr_ty") === (!!value || value === 0)
) {
if (!!getFieldValue("mod_lbr_ty") === !!value) {
return Promise.resolve();
}
return Promise.reject(

View File

@@ -1,80 +0,0 @@
import React, { useMemo } from "react";
import { Row, Col, Tag, Tooltip } from "antd";
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)(JobPartsQueueCount);
export function JobPartsQueueCount({ bodyshop, parts, style }) {
const partsStatus = useMemo(() => {
if (!parts) return null;
return parts.reduce(
(acc, val) => {
if (val.part_type === "PAS" || val.part_type === "PASL") return acc;
acc.total = acc.total + val.count;
acc[val.status] = acc[val.status] + val.count;
return acc;
},
{
total: 0,
null: 0,
[bodyshop.md_order_statuses.default_bo]: 0,
[bodyshop.md_order_statuses.default_ordered]: 0,
[bodyshop.md_order_statuses.default_received]: 0,
[bodyshop.md_order_statuses.default_returned]: 0,
}
);
}, [bodyshop, parts]);
if (!parts) return null;
return (
<Row style={style}>
<Col span={4}>
<Tooltip title="Total">
<Tag>{partsStatus.total}</Tag>
</Tooltip>
</Col>
<Col span={4}>
<Tooltip title="No Status">
<Tag color="gold">{partsStatus["null"]}</Tag>
</Tooltip>
</Col>
<Col span={4}>
<Tooltip title={bodyshop.md_order_statuses.default_ordered}>
<Tag color="blue">
{partsStatus[bodyshop.md_order_statuses.default_ordered]}
</Tag>
</Tooltip>
</Col>
<Col span={4}>
<Tooltip title={bodyshop.md_order_statuses.default_received}>
<Tag color="green">
{partsStatus[bodyshop.md_order_statuses.default_received]}
</Tag>
</Tooltip>
</Col>
<Col span={4}>
<Tooltip title={bodyshop.md_order_statuses.default_returned}>
<Tag color="orange">
{partsStatus[bodyshop.md_order_statuses.default_returned]}
</Tag>
</Tooltip>
</Col>
<Col span={4}>
<Tooltip title={bodyshop.md_order_statuses.default_bo}>
<Tag color="red">
{partsStatus[bodyshop.md_order_statuses.default_bo]}
</Tag>
</Tooltip>
</Col>
</Row>
);
}

View File

@@ -22,8 +22,7 @@ export default function JobReconciliationModalComponent({ job, bills }) {
(j.part_type !== null && j.part_type !== "PAE") ||
(j.line_desc &&
j.line_desc.toLowerCase().includes("towing") &&
j.lbr_op === "OP13") ||
j.db_ref === "936004" //ADD SHIPPING LINE.
j.lbr_op === "OP13")
);
return (

View File

@@ -1,6 +1,5 @@
import i18next from "i18next";
import _ from "lodash";
export const reconcileByAssocLine = (
jobLines,
jobLineState,
@@ -74,12 +73,7 @@ export const reconcileByPrice = (
jobLines.forEach((jl) => {
const matchingBillLineIds = billLines
.filter(
(bl) =>
bl.actual_price === jl.act_price &&
bl.quantity === jl.part_qty &&
!jl.removed
)
.filter((bl) => bl.actual_price === jl.act_price && bl.quantity === jl.part_qty && !jl.removed)
.map((bl) => bl.id);
if (matchingBillLineIds.length > 1) {

View File

@@ -1,23 +1,24 @@
import { useMutation } from "@apollo/client";
import { Checkbox, notification, Space, Spin } from "antd";
import { Button, notification } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { useMutation } from "@apollo/client";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { useTranslation } from "react-i18next";
export default function JobRemoveFromPartsQueue({ checked, jobId }) {
export default function JobRemoveFromPartsQueue({ jobId, refetch }) {
const [updateJob] = useMutation(UPDATE_JOB);
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const handleChange = async (e) => {
const handleClick = async (e) => {
setLoading(true);
const result = await updateJob({
variables: { jobId: jobId, job: { queued_for_parts: e.target.checked } },
variables: { jobId: jobId, job: { queued_for_parts: false } },
});
if (!!!result.errors) {
notification["success"]({ message: t("jobs.successes.save") });
if (refetch) refetch();
} else {
notification["error"]({
message: t("jobs.errors.saving", {
@@ -29,9 +30,8 @@ export default function JobRemoveFromPartsQueue({ checked, jobId }) {
};
return (
<Space>
<Checkbox checked={checked} onChange={handleChange} />
{loading && <Spin size="small" />}
</Space>
<Button onClick={handleClick} loading={loading}>
{t("general.actions.remove")}
</Button>
);
}

View File

@@ -169,7 +169,7 @@ export default function ScoreboardAddButton({
};
return (
<Popover content={overlay} visible={visibility} placement="bottom">
<Popover content={overlay} visible={visibility}>
<Button
loading={loading}
disabled={disabled}

View File

@@ -1,7 +1,8 @@
import { LoadingOutlined } from "@ant-design/icons";
import { useLazyQuery } from "@apollo/client";
import { Select, Space, Tag } from "antd";
import { Empty, Select, Space, Tag } from "antd";
import _ from "lodash";
import React, { forwardRef, useState, useEffect } from "react";
import React, { forwardRef, useEffect } from "react";
import { useTranslation } from "react-i18next";
import {
SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE,
@@ -23,35 +24,31 @@ const JobSearchSelect = (
ref
) => {
const { t } = useTranslation();
const [theOptions, setTheOptions] = useState([]);
const [callSearch, { loading, error, data }] = useLazyQuery(
SEARCH_JOBS_FOR_AUTOCOMPLETE,
{}
{
...(convertedOnly || notExported
? {
variables: {
...(convertedOnly ? { isConverted: true } : {}),
...(notExported ? { notExported: true } : {}),
...(notInvoiced ? { notInvoiced: true } : {}),
},
}
: {}),
}
);
const [callIdSearch, { loading: idLoading, error: idError, data: idData }] =
useLazyQuery(SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE);
const executeSearch = (v) => {
if (v && v !== "") callSearch(v);
callSearch(v);
};
const debouncedExecuteSearch = _.debounce(executeSearch, 500);
const handleSearch = (value) => {
debouncedExecuteSearch({
variables: {
search: value,
...(convertedOnly || notExported
? {
variables: {
...(convertedOnly ? { isConverted: true } : {}),
...(notExported ? { notExported: true } : {}),
...(notInvoiced ? { notInvoiced: true } : {}),
},
}
: {}),
},
});
debouncedExecuteSearch({ variables: { search: value } });
};
useEffect(() => {
@@ -60,17 +57,13 @@ const JobSearchSelect = (
}
}, [restProps.value, callIdSearch]);
useEffect(() => {
setTheOptions(
_.uniqBy(
[
...(idData && idData.jobs_by_pk ? [idData.jobs_by_pk] : []),
...(data && data.search_jobs ? data.search_jobs : []),
],
"id"
)
);
}, [data, idData]);
const theOptions = _.uniqBy(
[
...(idData && idData.jobs_by_pk ? [idData.jobs_by_pk] : []),
...(data && data.search_jobs ? data.search_jobs : []),
],
"id"
);
return (
<div>
@@ -84,8 +77,7 @@ const JobSearchSelect = (
}}
filterOption={false}
onSearch={handleSearch}
loading={loading || idLoading}
//notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
{...restProps}
>
{theOptions
@@ -107,6 +99,7 @@ const JobSearchSelect = (
))
: null}
</Select>
{idLoading || loading ? <LoadingOutlined /> : null}
{error ? <AlertComponent message={error.message} type="error" /> : null}
{idError ? (
<AlertComponent message={idError.message} type="error" />

View File

@@ -6,20 +6,17 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminStatus);
export function JobsAdminStatus({ insertAuditTrail, bodyshop, job }) {
export function JobsAdminStatus({ bodyshop, job }) {
const { t } = useTranslation();
const [mutationUpdateJobstatus] = useMutation(UPDATE_JOB_STATUS);
@@ -29,10 +26,6 @@ export function JobsAdminStatus({ insertAuditTrail, bodyshop, job }) {
})
.then((r) => {
notification["success"]({ message: t("jobs.successes.save") });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobstatuschange(status),
});
// refetch();
})
.catch((error) => {

View File

@@ -1,4 +1,3 @@
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import { useMutation } from "@apollo/client";
import { Button, Form, notification } from "antd";
import React, { useEffect, useState } from "react";
@@ -8,27 +7,8 @@ import DateTimePicker from "../form-date-time-picker/form-date-time-picker.compo
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import moment from "moment";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { insertAuditTrail } from "../../redux/application/application.actions";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsAdminDatesChange);
export function JobsAdminDatesChange({ insertAuditTrail, job }) {
export default function JobsAdminDatesChange({ job }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
@@ -38,25 +18,6 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
setLoading(true);
const result = await updateJob({
variables: { jobId: job.id, job: values },
refetchQueries: ['GET_JOB_BY_PK'],
awaitRefetchQueries:true
});
const changedAuditFields = form.getFieldsValue(
true,
(meta) => meta && meta.touched
);
Object.keys(changedAuditFields).forEach((key) => {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobfieldchange(
key,
changedAuditFields[key] instanceof moment
? moment(changedAuditFields[key]).format("MM/DD/YYYY hh:mm a")
: changedAuditFields[key]
),
});
});
if (!!!result.errors) {
@@ -68,8 +29,6 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
}),
});
}
form.resetFields();
form.resetFields();
setLoading(false);
//Get the owner details, populate it all back into the job.
};
@@ -92,7 +51,6 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
: null,
}}
>
<FormFieldsChanged form={form} />
<LayoutFormRow header={t("jobs.forms.estdates")}>
<Form.Item
label={t("jobs.fields.date_estimated")}

View File

@@ -6,36 +6,22 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import moment from "moment";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobAdminMarkReexport);
export function JobAdminMarkReexport({
insertAuditTrail,
bodyshop,
currentUser,
job,
}) {
export function JobAdminMarkReexport({ bodyshop, job }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
const [markJobForReexport] = useMutation(gql`
mutation MARK_JOB_FOR_REEXPORT($jobId: uuid!) {
update_jobs_by_pk(
@@ -92,10 +78,6 @@ export function JobAdminMarkReexport({
if (!result.errors) {
notification["success"]({ message: t("jobs.successes.save") });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobmarkforreexport(),
});
} else {
notification["error"]({
message: t("jobs.errors.saving", {
@@ -112,26 +94,8 @@ export function JobAdminMarkReexport({
variables: { jobId: job.id, date_exported: moment() },
});
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: job.id,
successful: true,
message: JSON.stringify([t("general.labels.markedexported")]),
useremail: currentUser.email,
},
],
},
});
if (!result.errors) {
notification["success"]({ message: t("jobs.successes.save") });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobmarkexported(),
});
} else {
notification["error"]({
message: t("jobs.errors.saving", {

View File

@@ -4,29 +4,21 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { insertAuditTrail } from "../../redux/application/application.actions";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminUnvoid);
export function JobsAdminUnvoid({
insertAuditTrail,
bodyshop,
job,
currentUser,
}) {
export function JobsAdminUnvoid({ bodyshop, job, currentUser }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [updateJob] = useMutation(gql`
@@ -92,11 +84,6 @@ mutation UNVOID_JOB($jobId: uuid!) {
if (!result.errors) {
notification["success"]({ message: t("jobs.successes.save") });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_unvoicejob(),
});
} else {
notification["error"]({
message: t("jobs.errors.saving", {

View File

@@ -45,11 +45,9 @@ export const GetSupplementDelta = async (client, jobId, newLines) => {
return acc + generateUpdateQuery(value, idx);
}, "");
const removeQueries = existingLines
.filter((l) => !l.manual_line)
.reduce((acc, value, idx) => {
return acc + generateRemoveQuery(value, idx);
}, "");
const removeQueries = existingLines.reduce((acc, value, idx) => {
return acc + generateRemoveQuery(value, idx);
}, "");
console.log(insertQueries, updateQueries, removeQueries);
if ((insertQueries + updateQueries + removeQueries).trim() === "") {

View File

@@ -15,6 +15,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { useHistory } from "react-router-dom";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
@@ -26,7 +27,6 @@ export function JobsCloseExportButton({
jobId,
disabled,
setSelectedJobs,
refetch,
}) {
const history = useHistory();
const { t } = useTranslation();
@@ -46,10 +46,13 @@ export function JobsCloseExportButton({
//Check if it's a QBO Setup.
let PartnerResponse;
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
PartnerResponse = await axios.post(`/qbo/receivables`, {
jobIds: [jobId],
elgen: true,
});
PartnerResponse = await axios.post(
`/qbo/receivables`,
{
jobIds: [jobId],
},
);
} else {
//Default is QBD
@@ -114,64 +117,58 @@ export function JobsCloseExportButton({
});
});
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
//QBO Logs are handled server side.
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: jobId,
successful: false,
message: JSON.stringify(
failedTransactions.map((ft) => ft.errorMessage)
),
useremail: currentUser.email,
},
],
},
});
}
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: jobId,
successful: false,
message: JSON.stringify(
failedTransactions.map((ft) => ft.errorMessage)
),
useremail: currentUser.email,
},
],
},
});
} else {
//Insert success export log.
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
//QBO Logs are handled server side.
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: jobId,
successful: true,
useremail: currentUser.email,
},
],
},
});
const jobUpdateResponse = await updateJob({
variables: {
jobId: jobId,
job: {
status: bodyshop.md_ro_statuses.default_exported || "Exported*",
date_exported: new Date(),
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: jobId,
successful: true,
useremail: currentUser.email,
},
},
});
],
},
});
if (!jobUpdateResponse.errors) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
} else {
notification["error"]({
message: t("jobs.errors.exporting", {
error: JSON.stringify(jobUpdateResponse.error),
}),
});
}
const jobUpdateResponse = await updateJob({
variables: {
jobId: jobId,
job: {
status: bodyshop.md_ro_statuses.default_exported || "Exported*",
date_exported: new Date(),
},
},
});
if (!jobUpdateResponse.errors) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
} else {
notification["error"]({
message: t("jobs.errors.exporting", {
error: JSON.stringify(jobUpdateResponse.error),
}),
});
}
if (setSelectedJobs) {
setSelectedJobs((selectedJobs) => {
@@ -179,7 +176,7 @@ export function JobsCloseExportButton({
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
setLoading(false);
};

View File

@@ -1,4 +1,4 @@
import { Form, Select, Space, Tooltip } from "antd";
import { Form, Select } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -8,7 +8,6 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import LaborTypeFormItem from "../form-items-formatted/labor-type-form-item.component";
import PartTypeFormItem from "../form-items-formatted/part-type-form-item.component";
import ReadOnlyFormItem from "../form-items-formatted/read-only-form-item.component";
import { WarningOutlined } from "@ant-design/icons";
import "./jobs-close-lines.styles.scss";
const mapStateToProps = createStructuredSelector({
@@ -63,23 +62,14 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
</Form.Item>
</td>
<td>
<Space>
<Form.Item
span={2}
// label={t("joblines.fields.act_price")}
key={`${index}act_price`}
name={[field.name, "act_price"]}
>
<ReadOnlyFormItem type="currency" />
</Form.Item>
<Form.Item
noStyle
key={`${index}convertedtolbr`}
name={[field.name, "convertedtolbr"]}
>
<HasBeenConvertedTolabor />
</Form.Item>
</Space>
<Form.Item
span={2}
// label={t("joblines.fields.act_price")}
key={`${index}act_price`}
name={[field.name, "act_price"]}
>
<ReadOnlyFormItem type="currency" />
</Form.Item>
</td>
<td>
<Form.Item
@@ -202,14 +192,3 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
);
}
export default connect(mapStateToProps, mapDispatchToProps)(JobsCloseLines);
const HasBeenConvertedTolabor = ({ value }) => {
const { t } = useTranslation();
console.log(value);
if (!value) return null;
return (
<Tooltip title={t("joblines.labels.convertedtolabor")}>
<WarningOutlined style={{ color: "tomato" }} />
</Tooltip>
);
};

View File

@@ -146,11 +146,7 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
</Form.Item>
</LayoutFormRow>
</Collapse.Panel>
<Collapse.Panel
forceRender
key="claim"
header={t("menus.jobsdetail.claimdetail")}
>
<Collapse.Panel forceRender key="claim" header={t("menus.jobsdetail.claimdetail")}>
<LayoutFormRow>
<Form.Item label={t("jobs.fields.loss_desc")} name="loss_desc">
<Input />
@@ -197,8 +193,7 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
</Form.Item>
</LayoutFormRow>
</Collapse.Panel>
<Collapse.Panel
forceRender
<Collapse.Panel forceRender
key="financial"
header={t("menus.jobsdetail.financials")}
>
@@ -209,7 +204,7 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<CurrencyInput min={0} />
</Form.Item>
<Form.Item label={t("jobs.fields.ded_status")} name="ded_status">
<Select allowClear>
<Select>
<Select.Option value="W">
{t("jobs.labels.deductible.waived")}
</Select.Option>

View File

@@ -4,7 +4,6 @@ import { Table, Input, Card, Space } from "antd";
import { Link } from "react-router-dom";
import { alphaSort } from "../../utils/sorters";
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
export default function JobsCreateVehicleInfoSearchComponent({
loading,
@@ -28,9 +27,7 @@ export default function JobsCreateVehicleInfoSearchComponent({
tableState.sortedInfo.columnKey === "v_vin" &&
tableState.sortedInfo.order,
render: (text, record) => (
<Link to={"/manage/vehicles/" + record.id}>
<VehicleVinDisplay>{record.v_vin}</VehicleVinDisplay>
</Link>
<Link to={"/manage/vehicles/" + record.id}>{record.v_vin}</Link>
),
},
{

View File

@@ -16,18 +16,15 @@ export function JobsDetailChangeFilehandler({ disabled, form, bodyshop }) {
};
const menu = (
<Menu
onClick={handleClick}
style={{
columnCount: Math.floor(bodyshop.md_filehandlers.length / 10) + 1,
}}
>
{bodyshop.md_filehandlers.map((est, idx) => (
<Menu.Item value={est} key={idx} style={{ breakInside: "avoid" }}>
{`${est.ins_ct_fn} ${est.ins_ct_ln}`}
</Menu.Item>
))}
</Menu>
<div>
<Menu onClick={handleClick}>
{bodyshop.md_filehandlers.map((est, idx) => (
<Menu.Item value={est} key={idx}>
{`${est.ins_ct_fn} ${est.ins_ct_ln}`}
</Menu.Item>
))}
</Menu>
</div>
);
return (

View File

@@ -1,10 +1,10 @@
import { Card, Col, Row, Space, Tag } from "antd";
import {
WarningFilled,
ExclamationCircleFilled,
PauseCircleOutlined,
WarningFilled,
} from "@ant-design/icons";
import { Card, Col, Row, Space, Tag } from "antd";
import React from "react";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
@@ -13,17 +13,16 @@ import { selectJobReadOnly } from "../../redux/application/application.selectors
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import DataLabel from "../data-label/data-label.component";
import JobAltTransportChange from "../job-at-change/job-at-change.component";
import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container";
import JobsRelatedRos from "../jobs-related-ros/jobs-related-ros.component";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
import "./jobs-detail-header.styles.scss";
import JobsRelatedRos from "../jobs-related-ros/jobs-related-ros.component";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
@@ -56,6 +55,12 @@ const colSpan = {
export function JobsDetailHeader({ job, bodyshop, disabled }) {
const { t } = useTranslation();
const jobInPostProduction = useMemo(() => {
return bodyshop.md_ro_statuses.post_production_statuses.includes(
job.status
);
}, [job.status, bodyshop.md_ro_statuses.post_production_statuses]);
const vehicleTitle = `${job.v_model_yr || ""} ${job.v_color || ""}
${job.v_make_desc || ""}
${job.v_model_desc || ""}`.trim();
@@ -123,10 +128,11 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
</DataLabel>
)}
<DataLabel label={t("jobs.fields.production_vars.note")}>
<ProductionListColumnProductionNote record={job} />
</DataLabel>
{(job.inproduction || jobInPostProduction) && (
<DataLabel label={t("jobs.fields.production_vars.note")}>
<ProductionListColumnProductionNote record={job} />
</DataLabel>
)}
<Space>
{job.special_coverage_policy && (
<Tag color="tomato">
@@ -174,11 +180,6 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
<DataLabel key="4" label={t("owners.fields.ownr_ea")}>
{job.ownr_ea || ""}
</DataLabel>
{job.owner?.tax_number && (
<DataLabel key="5" label={t("owners.fields.tax_number")}>
{job.owner?.tax_number || ""}
</DataLabel>
)}
</div>
</Card>
</Col>
@@ -210,39 +211,11 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
}`})`}
</DataLabel>
<DataLabel key="4" label={t("vehicles.fields.v_vin")}>
<VehicleVinDisplay>
{`${job.v_vin || t("general.labels.na")}`}
</VehicleVinDisplay>
{`${job.v_vin || t("general.labels.na")}`}
</DataLabel>
<DataLabel label={t("jobs.labels.relatedros")}>
<JobsRelatedRos jobid={job.id} job={job} />
</DataLabel>
{job.vehicle && job.vehicle.notes && (
<DataLabel
label={t("vehicles.fields.notes")}
valueStyle={{ whiteSpace: "pre-wrap" }}
>
{job.vehicle.notes}
</DataLabel>
)}
{job.vehicle && job.vehicle.v_paint_codes && (
<DataLabel
label={t("vehicles.fields.v_paint_codes", { number: "" })}
>
<span style={{ whiteSpace: "pre" }}>
{Object.keys(job.vehicle.v_paint_codes)
.filter(
(key) =>
job.vehicle.v_paint_codes[key] !== "" &&
job.vehicle.v_paint_codes[key] !== null &&
job.vehicle.v_paint_codes[key] !== undefined
)
.map((key, idx) => (
<Tag key={idx}>{job.vehicle.v_paint_codes[key]}</Tag>
))}
</span>
</DataLabel>
)}
</div>
</Card>
</Col>

View File

@@ -97,7 +97,7 @@ export function JobsDetailRates({ jobRO, form, job, bodyshop }) {
</Form.Item>
<Form.Item
noStyle
nostyle
shouldUpdate={(prev, cur) => prev.auto_add_ats !== cur.auto_add_ats}
>
{() => {

View File

@@ -1,5 +1,5 @@
import { EditFilled, FileExcelFilled, SyncOutlined } from "@ant-design/icons";
import { Button, Card, Col, Row, Space } from "antd";
import { FileExcelFilled, EditFilled, SyncOutlined } from "@ant-design/icons";
import { Card, Col, Row, Space, Button } from "antd";
import React, { useEffect, useState } from "react";
import Gallery from "react-grid-gallery";
import { useTranslation } from "react-i18next";

View File

@@ -25,7 +25,6 @@ function JobsDocumentGalleryExternal({
id: value.id,
type: value.type,
tags: [{ value: value.type, title: value.type }],
size: value.size,
});
}

View File

@@ -1,152 +0,0 @@
import { SyncOutlined, FileExcelFilled } from "@ant-design/icons";
import { Button, Card, Space } from "antd";
import React, { useEffect } from "react";
import Gallery from "react-grid-gallery";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
getBillMedia,
getJobMedia,
toggleMediaSelected,
} from "../../redux/media/media.actions";
import { selectAllMedia } from "../../redux/media/media.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { CreateExplorerLinkForJob } from "../../utils/localmedia";
import DocumentsLocalUploadComponent from "../documents-local-upload/documents-local-upload.component";
import JobsLocalGalleryDownloadButton from "./jobs-documents-local-gallery.download";
import JobsDocumentsLocalGalleryReassign from "./jobs-documents-local-gallery.reassign.component";
import JobsDocumentsLocalGallerySelectAllComponent from "./jobs-documents-local-gallery.selectall.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
allMedia: selectAllMedia,
});
const mapDispatchToProps = (dispatch) => ({
getJobMedia: (id) => dispatch(getJobMedia(id)),
getBillMedia: ({ jobid, invoice_number }) => {
dispatch(getBillMedia({ jobid, invoice_number }));
},
toggleMediaSelected: ({ jobid, filename }) =>
dispatch(toggleMediaSelected({ jobid, filename })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsDocumentsLocalGallery);
export function JobsDocumentsLocalGallery({
bodyshop,
toggleMediaSelected,
getJobMedia,
getBillMedia,
allMedia,
job,
invoice_number,
vendorid,
}) {
const { t } = useTranslation();
useEffect(() => {
if (job) {
if (invoice_number) {
getBillMedia({ jobid: job.id, invoice_number });
} else {
getJobMedia(job.id);
}
}
}, [job, invoice_number, getJobMedia, getBillMedia]);
const jobMedia =
allMedia && allMedia[job.id]
? allMedia[job.id].reduce(
(acc, val) => {
if (
val.type &&
val.type.mime &&
val.type.mime.startsWith("image")
) {
acc.images.push(val);
} else {
acc.other.push(val);
}
return acc;
},
{ images: [], other: [] }
)
: { images: [], other: [] };
return (
<div>
<Space wrap>
<Button
onClick={() => {
if (job) {
if (invoice_number) {
getBillMedia({ jobid: job.id, invoice_number });
} else {
getJobMedia(job.id);
}
}
}}
>
<SyncOutlined />
</Button>
<a href={CreateExplorerLinkForJob({ jobid: job.id })}>
<Button>{t("documents.labels.openinexplorer")}</Button>
</a>
<JobsDocumentsLocalGalleryReassign jobid={job.id} />
<JobsDocumentsLocalGallerySelectAllComponent jobid={job.id} />
<JobsLocalGalleryDownloadButton job={job} />
</Space>
<Card>
<DocumentsLocalUploadComponent
job={job}
invoice_number={invoice_number}
vendorid={vendorid}
/>
</Card>
<Card title={t("jobs.labels.documents-images")}>
<Gallery
images={jobMedia.images}
backdropClosesModal={true}
onSelectImage={(index, image) => {
toggleMediaSelected({ jobid: job.id, filename: image.filename });
}}
onClickImage={(props) => {
window.open(
props.target.src,
"_blank",
"toolbar=0,location=0,menubar=0"
);
}}
/>
</Card>
<Card title={t("jobs.labels.documents-other")}>
<Gallery
images={jobMedia.other}
backdropClosesModal={true}
enableLightbox={false}
thumbnailStyle={() => {
return {
backgroundImage: <FileExcelFilled />,
height: "100%",
width: "100%",
cursor: "pointer",
};
}}
onClickThumbnail={(index) => {
window.open(
jobMedia.other[index].src,
"_blank",
"toolbar=0,location=0,menubar=0"
);
}}
onSelectImage={(index, image) => {
toggleMediaSelected({ jobid: job.id, filename: image.filename });
}}
/>
</Card>
</div>
);
}

View File

@@ -1,74 +0,0 @@
import { Button } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import cleanAxios from "../../utils/CleanAxios";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectAllMedia } from "../../redux/media/media.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
allMedia: selectAllMedia,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsLocalGalleryDownloadButton);
export function JobsLocalGalleryDownloadButton({
bodyshop,
galleryImages,
allMedia,
job,
}) {
const { t } = useTranslation();
const [download, setDownload] = useState(null);
function downloadProgress(progressEvent) {
setDownload((currentDownloadState) => {
return {
downloaded: progressEvent.loaded || 0,
speed:
(progressEvent.loaded || 0) -
((currentDownloadState && currentDownloadState.downloaded) || 0),
};
});
}
const handleDownload = async () => {
const theDownloadedZip = await cleanAxios.post(
`${bodyshop.localmediaserverhttp}/jobs/download`,
{
jobid: job.id,
files: ((allMedia && allMedia[job.id]) || [])
.filter((i) => i.isSelected)
.map((i) => i.filename),
},
{
headers: { ims_token: bodyshop.localmediatoken },
responseType: "arraybuffer",
onDownloadProgress: downloadProgress,
}
);
setDownload(null);
standardMediaDownload(theDownloadedZip.data, job.ro_number);
};
return (
<Button loading={!!download} onClick={handleDownload}>
{t("documents.actions.download")}
</Button>
);
}
function standardMediaDownload(bufferData, filename) {
const a = document.createElement("a");
const url = window.URL.createObjectURL(new Blob([bufferData]));
a.href = url;
a.download = `${filename}.zip`;
a.click();
}

View File

@@ -1,96 +0,0 @@
import { Button, Form, Popover, Space } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { getJobMedia } from "../../redux/media/media.actions";
import { selectAllMedia } from "../../redux/media/media.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import cleanAxios from "../../utils/CleanAxios";
import JobSearchSelect from "../job-search-select/job-search-select.component";
const mapStateToProps = createStructuredSelector({
allMedia: selectAllMedia,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
getJobMedia: (id) => dispatch(getJobMedia(id)),
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsDocumentsLocalGalleryReassign);
export function JobsDocumentsLocalGalleryReassign({
bodyshop,
jobid,
allMedia,
getJobMedia,
}) {
const { t } = useTranslation();
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const handleFinish = async ({ jobid: newJobid }) => {
setLoading(true);
const selectedDocuments = allMedia[jobid].filter((m) => m.isSelected);
await cleanAxios.post(
`${bodyshop.localmediaserverhttp}/jobs/move`,
{
from_jobid: jobid,
jobid: newJobid,
files: selectedDocuments.map((f) => f.filename),
},
{ headers: { ims_token: bodyshop.localmediatoken } }
);
getJobMedia(jobid);
setVisible(false);
setLoading(false);
};
const popContent = (
<div>
<Form onFinish={handleFinish} layout="vertical" form={form}>
<Form.Item
label={t("documents.labels.newjobid")}
style={{ width: "20rem" }}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={"jobid"}
>
<JobSearchSelect />
</Form.Item>
</Form>
<Space>
<Button type="primary" onClick={() => form.submit()}>
{t("general.actions.submit")}
</Button>
<Button onClick={() => setVisible(false)}>
{t("general.actions.cancel")}
</Button>
</Space>
</div>
);
return (
<Popover content={popContent} visible={visible}>
<Button
//disabled={selectedImages.length < 1}
onClick={() => setVisible(true)}
loading={loading}
>
{t("documents.actions.reassign")}
</Button>
</Popover>
);
}

View File

@@ -1,53 +0,0 @@
import { Button, Space } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
selectAllmediaForJob,
deselectAllMediaForJob,
} from "../../redux/media/media.actions";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
selectAllmediaForJob: (jobid) => dispatch(selectAllmediaForJob(jobid)),
deselectAllmediaForJob: (jobid) => dispatch(deselectAllMediaForJob(jobid)),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsDocumentsLocalGallerySelectAllComponent);
export function JobsDocumentsLocalGallerySelectAllComponent({
jobid,
selectAllmediaForJob,
deselectAllmediaForJob,
}) {
const { t } = useTranslation();
// onSelectImage={(index, image) => {
// toggleMediaSelected({ jobid: job.id, filename: image.filename });
// }}
const handleSelectAll = () => {
selectAllmediaForJob({ jobid });
};
const handleDeselectAll = () => {
deselectAllmediaForJob({ jobid });
};
return (
<Space wrap>
<Button onClick={handleSelectAll}>
{t("general.actions.selectall")}
</Button>
<Button onClick={handleDeselectAll}>
{t("general.actions.deselectall")}
</Button>
</Space>
);
}

View File

@@ -26,7 +26,6 @@ export function JobsExportAllButton({
disabled,
loadingCallback,
completedCallback,
refetch,
}) {
const { t } = useTranslation();
const [updateJob] = useMutation(UPDATE_JOBS);
@@ -40,7 +39,6 @@ export function JobsExportAllButton({
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
PartnerResponse = await axios.post(`/qbo/receivables`, {
jobIds: jobIds,
elgen: true,
});
} else {
let QbXmlResponse;
@@ -85,7 +83,6 @@ export function JobsExportAllButton({
return;
}
}
console.log("PartnerResponse", PartnerResponse);
const groupedData = _.groupBy(
PartnerResponse.data,
@@ -109,70 +106,61 @@ export function JobsExportAllButton({
});
//Call is not awaited as it is not critical to finish before proceeding.
});
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
//QBO Logs are handled server side.
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: key,
successful: false,
message: JSON.stringify(
failedTransactions.map((ft) => ft.errorMessage)
),
useremail: currentUser.email,
},
],
},
});
}
} else {
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
//QBO Logs are handled server side.
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: key,
successful: true,
useremail: currentUser.email,
},
],
},
});
const jobUpdateResponse = await updateJob({
variables: {
jobIds: [key],
fields: {
status:
bodyshop.md_ro_statuses.default_exported || "Exported*",
date_exported: new Date(),
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: key,
successful: false,
message: JSON.stringify(
failedTransactions.map((ft) => ft.errorMessage)
),
useremail: currentUser.email,
},
},
});
],
},
});
} else {
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: key,
successful: true,
useremail: currentUser.email,
},
],
},
});
if (!jobUpdateResponse.errors) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
} else {
notification["error"]({
message: t("jobs.errors.exporting", {
error: JSON.stringify(jobUpdateResponse.error),
}),
});
}
const jobUpdateResponse = await updateJob({
variables: {
jobIds: [key],
fields: {
status: bodyshop.md_ro_statuses.default_exported || "Exported*",
date_exported: new Date(),
},
},
});
if (!jobUpdateResponse.errors) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
} else {
notification["error"]({
message: t("jobs.errors.exporting", {
error: JSON.stringify(jobUpdateResponse.error),
}),
});
}
}
})
);
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
if (!!completedCallback) completedCallback([]);
if (!!loadingCallback) loadingCallback(false);

View File

@@ -77,12 +77,6 @@ export function JobsList({ bodyshop }) {
(j.v_model_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.est_ct_fn || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.est_ct_ln || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.v_make_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase())
@@ -143,9 +137,9 @@ export function JobsList({ bodyshop }) {
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.ownerid ? (
return record.owner ? (
<Link
to={"/manage/owners/" + record.ownerid}
to={"/manage/owners/" + record.owner.id}
onClick={(e) => e.stopPropagation()}
>
<OwnerNameDisplay ownerObject={record} />
@@ -270,32 +264,6 @@ export function JobsList({ bodyshop }) {
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
),
},
{
title: t("jobs.labels.estimator"),
dataIndex: "jobs.labels.estimator",
key: "jobs.labels.estimator",
ellipsis: true,
responsive: ["xl"],
filterSearch: true,
filters:
(jobs &&
jobs
.map((j) => `${j.est_ct_fn || ""} ${j.est_ct_ln || ""}`.trim())
.filter(onlyUnique)
.map((s) => {
return {
text: s || "N/A",
value: [s],
};
})) ||
[],
onFilter: (value, record) =>
value.includes(
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim()
),
render: (text, record) =>
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim(),
},
{
title: t("jobs.fields.comment"),
dataIndex: "comment",

View File

@@ -61,9 +61,6 @@ export function JobNotesContainer({ jobId, insertAuditTrail }) {
jobId={jobId}
loading={loading}
data={data ? data.jobs_by_pk.notes : null}
relatedRos={
data ? data.jobs_by_pk.vehicle && data.jobs_by_pk.vehicle.jobs : null
}
refetch={refetch}
deleteLoading={deleteLoading}
handleNoteDelete={handleNoteDelete}

View File

@@ -14,7 +14,6 @@ import { selectJobReadOnly } from "../../redux/application/application.selectors
import { setModalContext } from "../../redux/modals/modals.actions";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants";
import useLocalStorage from "../../utils/useLocalStorage";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import NoteUpsertModal from "../note-upsert-modal/note-upsert-modal.container";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
@@ -38,11 +37,8 @@ export function JobNotesComponent({
setNoteUpsertContext,
deleteLoading,
ro_number,
relatedRos,
}) {
const { t } = useTranslation();
const [filter, setFilter] = useLocalStorage("filter_job_notes_icons", null);
const Templates = TemplateList("job_special", {
ro_number,
});
@@ -53,18 +49,6 @@ export function JobNotesComponent({
dataIndex: "icons",
key: "icons",
width: 80,
filteredValue: filter?.icons || null,
filters: [
{
text: t("notes.labels.usernotes"),
value: false,
},
{
text: t("notes.labels.systemnotes"),
value: true,
},
],
onFilter: (value, record) => record.audit === value,
render: (text, record) => (
<span>
{record.critical ? (
@@ -146,10 +130,6 @@ export function JobNotesComponent({
},
];
const handleTableChange = (pagination, filters, sorter) => {
setFilter(filters);
};
return (
<div>
<LayoutFormRow>
@@ -169,7 +149,6 @@ export function JobNotesComponent({
actions: { refetch: refetch },
context: {
jobId: jobId,
relatedRos: relatedRos,
},
});
}}
@@ -185,7 +164,6 @@ export function JobNotesComponent({
columns={columns}
rowKey="id"
dataSource={data}
onChange={handleTableChange}
/>
</Card>
</div>

View File

@@ -1,387 +0,0 @@
import {
ExclamationCircleFilled,
PauseCircleOutlined,
SyncOutlined,
} from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { Button, Card, Grid, Input, Space, Table } from "antd";
import queryString from "query-string";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { onlyUnique } from "../../utils/arrayHelper";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters";
import AlertComponent from "../alert/alert.component";
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
export function JobsReadyList({ bodyshop }) {
const searchParams = queryString.parse(useLocation().search);
const { selected } = searchParams;
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const readyStatuses = useMemo(() => {
if (bodyshop.md_ro_statuses.ready_statuses)
return bodyshop.md_ro_statuses.ready_statuses;
return bodyshop.md_ro_statuses.post_production_statuses.filter(
(s) =>
s !== bodyshop.md_ro_statuses.default_invoiced &&
s !== bodyshop.md_ro_statuses.default_exported
);
}, [bodyshop.md_ro_statuses]);
const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
variables: {
statuses: readyStatuses,
},
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
});
const { t } = useTranslation();
const history = useHistory();
const [searchText, setSearchText] = useState("");
if (error) return <AlertComponent message={error.message} type="error" />;
const jobs = data
? searchText === ""
? data.jobs
: data.jobs.filter(
(j) =>
(j.ro_number || "")
.toString()
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.ownr_co_nm || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.comments || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.ownr_fn || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.ownr_ln || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.clm_no || "").toLowerCase().includes(searchText.toLowerCase()) ||
(j.plate_no || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.v_model_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.est_ct_fn || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.est_ct_ln || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.v_make_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase())
)
: [];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const handleOnRowClick = (record) => {
if (record) {
if (record.id) {
history.push({
search: queryString.stringify({
...searchParams,
selected: record.id,
}),
});
}
}
};
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
<Link
to={"/manage/jobs/" + record.id}
onClick={(e) => e.stopPropagation()}
>
<Space>
{record.ro_number || t("general.labels.na")}
{record.production_vars && record.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
</Space>
</Link>
),
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.owner ? (
<Link
to={"/manage/owners/" + record.owner.id}
onClick={(e) => e.stopPropagation()}
>
<OwnerNameDisplay ownerObject={record} />
</Link>
) : (
<span>
<OwnerNameDisplay ownerObject={record} />
</span>
);
},
},
{
title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph1} jobid={record.id} />
),
},
{
title: t("jobs.fields.ownr_ph2"),
dataIndex: "ownr_ph2",
key: "ownr_ph2",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph2} jobid={record.id} />
),
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
ellipsis: true,
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filters:
(jobs &&
jobs
.map((j) => j.status)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
value: [s],
};
})) ||
[],
onFilter: (value, record) => value.includes(record.status),
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
render: (text, record) => {
return record.vehicleid ? (
<Link
to={"/manage/vehicles/" + record.vehicleid}
onClick={(e) => e.stopPropagation()}
>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</Link>
) : (
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}</span>
);
},
},
{
title: t("vehicles.fields.plate_no"),
dataIndex: "plate_no",
key: "plate_no",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
sortOrder:
state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
},
{
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder:
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
render: (text, record) =>
`${record.clm_no || ""}${
record.po_number ? ` (PO: ${record.po_number})` : ""
}`,
},
{
title: t("jobs.fields.ins_co_nm"),
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
responsive: ["md"],
},
{
title: t("jobs.fields.clm_total"),
dataIndex: "clm_total",
key: "clm_total",
responsive: ["md"],
ellipsis: true,
sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
),
},
{
title: t("jobs.labels.estimator"),
dataIndex: "jobs.labels.estimator",
key: "jobs.labels.estimator",
ellipsis: true,
responsive: ["xl"],
filterSearch: true,
filters:
(jobs &&
jobs
.map((j) => `${j.est_ct_fn || ""} ${j.est_ct_ln || ""}`.trim())
.filter(onlyUnique)
.map((s) => {
return {
text: s || "N/A",
value: [s],
};
})) ||
[],
onFilter: (value, record) =>
value.includes(
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim()
),
render: (text, record) =>
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim(),
},
{
title: t("jobs.fields.comment"),
dataIndex: "comment",
key: "comment",
ellipsis: true,
responsive: ["md"],
},
// {
// title: t("jobs.fields.owner_owing"),
// dataIndex: "owner_owing",
// key: "owner_owing",
// responsive: ["md"],
// render: (text, record) => (
// <CurrencyFormatter>{record.owner_owing}</CurrencyFormatter>
// ),
// },
];
const scrollMapper = {
xs: true,
sm: true,
md: true,
lg: "100%",
xl: "100%",
xxl: "100%",
};
return (
<Card
title={t("titles.bc.jobs-ready")}
extra={
<Space wrap>
<span>({readyStatuses && readyStatuses.join(", ")})</span>
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<Input.Search
placeholder={t("general.labels.search")}
onChange={(e) => {
setSearchText(e.target.value);
}}
value={searchText}
enterButton
/>
</Space>
}
>
<Table
loading={loading}
pagination={{ defaultPageSize: 50 }}
columns={columns}
rowKey="id"
dataSource={jobs}
scroll={{
x: selectedBreakpoint ? scrollMapper[selectedBreakpoint[0]] : "100%",
}}
rowSelection={{
onSelect: (record) => {
handleOnRowClick(record);
},
selectedRowKeys: [selected],
type: "radio",
}}
onChange={handleTableChange}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {
handleOnRowClick(record);
},
};
}}
/>
</Card>
);
}
export default connect(mapStateToProps, null)(JobsReadyList);

View File

@@ -12,24 +12,7 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(LaborAllocationsAdjustmentEdit);
export function LaborAllocationsAdjustmentEdit({
insertAuditTrail,
export default function LaborAllocationsAdjustmentEdit({
jobId,
mod_lbr_ty,
adjustments,
@@ -68,15 +51,6 @@ export function LaborAllocationsAdjustmentEdit({
notification["success"]({
message: t("jobs.successes.save"),
});
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobmodifylbradj({
mod_lbr_ty: values.mod_lbr_ty,
hours:
values.hours -
((adjustments && adjustments[mod_lbr_ty]) || 0).toFixed(1),
}),
});
}
setLoading(false);
setVisible(false);

View File

@@ -1,18 +1,16 @@
import { EditFilled } from "@ant-design/icons";
import { Card, Col, Row, Space, Table } from "antd";
import _ from "lodash";
import React, { useEffect, useMemo, useState } from "react";
import { Card, Space, Table } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { alphaSort } from "../../utils/sorters";
import LaborAllocationsAdjustmentEdit from "../labor-allocations-adjustment-edit/labor-allocations-adjustment-edit.component";
import "./labor-allocations-table.styles.scss";
import { CalculateAllocationsTotals } from "./labor-allocations-table.utility";
import _ from "lodash";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
technician: selectTechnician,
@@ -45,11 +43,6 @@ export function LaborAllocationsTable({
if (!jobId) setTotals([]);
}, [joblines, timetickets, bodyshop, adjustments, jobId]);
const convertedLines = useMemo(
() => joblines && joblines.filter((j) => j.convertedtolbr),
[joblines]
);
const columns = [
{
title: t("timetickets.fields.cost_center"),
@@ -121,105 +114,24 @@ export function LaborAllocationsTable({
),
},
];
const convertedTableCols = [
{
title: t("joblines.fields.line_desc"),
dataIndex: "line_desc",
key: "line_desc",
ellipsis: true,
},
{
title: t("joblines.fields.op_code_desc"),
dataIndex: "op_code_desc",
key: "op_code_desc",
ellipsis: true,
render: (text, record) =>
`${record.op_code_desc || ""}${
record.alt_partm ? ` ${record.alt_partm}` : ""
}`,
},
{
title: t("joblines.fields.act_price"),
dataIndex: "act_price",
key: "act_price",
ellipsis: true,
render: (text, record) => (
<>
<CurrencyFormatter>
{record.db_ref === "900510" || record.db_ref === "900511"
? record.prt_dsmk_m
: record.act_price}
</CurrencyFormatter>
{record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? (
<span
style={{ marginLeft: ".2rem" }}
>{`(${record.prt_dsmk_p}%)`}</span>
) : (
<></>
)}
</>
),
},
{
title: t("joblines.fields.part_qty"),
dataIndex: "part_qty",
key: "part_qty",
},
{
title: t("joblines.fields.mod_lbr_ty"),
dataIndex: "conv_mod_lbr_ty",
key: "conv_mod_lbr_ty",
render: (text, record) =>
record.convertedtolbr_data && record.convertedtolbr_data.mod_lbr_ty,
},
{
title: t("joblines.fields.mod_lb_hrs"),
dataIndex: "conv_mod_lb_hrs",
key: "conv_mod_lb_hrs",
render: (text, record) =>
record.convertedtolbr_data &&
record.convertedtolbr_data.mod_lb_hrs &&
record.convertedtolbr_data.mod_lb_hrs.toFixed(1),
},
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<Row gutter={[16, 16]}>
<Col span={24}>
<Card title={t("jobs.labels.laborallocations")}>
<Table
columns={columns}
rowKey="cost_center"
pagination={false}
onChange={handleTableChange}
dataSource={totals}
scroll={{
x: true,
}}
/>
</Card>
</Col>
{convertedLines && convertedLines.length > 0 && (
<Col span={24}>
<Card title={t("jobs.labels.convertedtolabor")}>
<Table
columns={convertedTableCols}
rowKey="id"
pagination={false}
dataSource={convertedLines}
scroll={{
x: true,
}}
/>
</Card>
</Col>
)}
</Row>
<Card title={t("jobs.labels.laborallocations")}>
<Table
columns={columns}
rowKey="cost_center"
pagination={false}
onChange={handleTableChange}
dataSource={totals}
scroll={{
x: true,
}}
/>
</Card>
);
}
export default connect(mapStateToProps, null)(LaborAllocationsTable);

View File

@@ -6,17 +6,12 @@ export const CalculateAllocationsTotals = (
timetickets,
adjustments = []
) => {
console.log(
"🚀 ~ file: labor-allocations-table.utility.js ~ line 9 ~ adjustments",
adjustments
);
const responsibilitycenters = bodyshop.md_responsibility_centers;
const jobCodes = joblines.map((item) => item.mod_lbr_ty);
//.filter((value, index, self) => self.indexOf(value) === index && !!value);
const ticketCodes = timetickets.map((item) => item.ciecacode);
//.filter((value, index, self) => self.indexOf(value) === index && !!value);
const adjustmentCodes = Object.keys(adjustments);
const allCodes = [...jobCodes, ...ticketCodes, ...adjustmentCodes].filter(
const allCodes = [...jobCodes, ...ticketCodes].filter(
(value, index, self) => self.indexOf(value) === index && !!value
);

View File

@@ -1,92 +1,51 @@
import { Checkbox, Col, Form, Input, Row, Space, Switch, Tag } from "antd";
import { Col, Form, Input, Row, Switch } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectNoteUpsert } from "../../redux/modals/modals.selectors";
import NotesPresetButton from "../notes-preset-button/notes-preset-button.component";
const mapStateToProps = createStructuredSelector({
noteUpsertModal: selectNoteUpsert,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(NoteUpsertModalComponent);
export function NoteUpsertModalComponent({ form, noteUpsertModal }) {
export default function NoteUpsertModalComponent({ form }) {
const { t } = useTranslation();
const { jobId, existingNote, relatedRos } = noteUpsertModal.context;
const filteredRelatedRos = relatedRos
? relatedRos.filter((j) => j.id !== jobId)
: [];
return (
<>
<Row gutter={[16, 16]}>
<Col span={8}>
<Form.Item
label={t("notes.fields.critical")}
name="critical"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
label={t("notes.fields.private")}
name="private"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Col>
<Col span={8}>
<NotesPresetButton form={form} />
</Col>
<Col span={24}>
<Form.Item
label={t("notes.fields.text")}
name="text"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input.TextArea
rows={8}
placeholder={t("notes.labels.newnoteplaceholder")}
/>
</Form.Item>
</Col>
</Row>
<div>
<div>{!existingNote && t("notes.labels.addtorelatedro")}</div>
{!existingNote &&
filteredRelatedRos.map((j, idx) => (
<Space key={j.id} align="center">
<Form.Item
noStyle
name={["relatedros", j.id]}
valuePropName="checked"
>
<Checkbox />
</Form.Item>
<Tag>
{`${j.ro_number || "N/A"}${j.clm_no ? ` | ${j.clm_no}` : ""}${
j.status ? ` | ${j.status}` : ""
}`}
</Tag>
</Space>
))}
</div>
</>
<Row gutter={[16, 16]}>
<Col span={8}>
<Form.Item
label={t("notes.fields.critical")}
name="critical"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
label={t("notes.fields.private")}
name="private"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Col>
<Col span={8}>
<NotesPresetButton form={form} />
</Col>
<Col span={24}>
<Form.Item
label={t("notes.fields.text")}
name="text"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input.TextArea
rows={8}
placeholder={t("notes.labels.newnoteplaceholder")}
/>
</Form.Item>
</Col>
</Row>
);
}

View File

@@ -4,14 +4,14 @@ import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_NEW_NOTE, UPDATE_NOTE } from "../../graphql/notes.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectNoteUpsert } from "../../redux/modals/modals.selectors";
import { selectCurrentUser } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import NoteUpsertModalComponent from "./note-upsert-modal.component";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
@@ -48,9 +48,7 @@ export function NoteUpsertModalContainer({
}
}, [existingNote, form, visible]);
const handleFinish = async (formValues) => {
const { relatedros, ...values } = formValues;
const handleFinish = (values) => {
if (existingNote) {
logImEXEvent("job_note_update");
@@ -72,44 +70,24 @@ export function NoteUpsertModalContainer({
toggleModalVisible();
} else {
logImEXEvent("job_note_insert");
const AdditionalNoteInserts = relatedros
? Object.keys(relatedros).filter((key) => relatedros[key])
: [];
await insertNote({
insertNote({
variables: {
noteInput: [
{ ...values, jobid: jobId, created_by: currentUser.email },
],
},
});
if (AdditionalNoteInserts.length > 0) {
//Insert the others.
AdditionalNoteInserts.forEach(async (newJobId) => {
await insertNote({
variables: {
noteInput: [
{ ...values, jobid: newJobId, created_by: currentUser.email },
],
},
});
insertAuditTrail({
jobid: newJobId,
operation: AuditTrailMapping.jobnoteadded(),
});
}).then((r) => {
if (refetch) refetch();
form.resetFields();
toggleModalVisible();
notification["success"]({
message: t("notes.successes.create"),
});
insertAuditTrail({
jobid: context.jobId,
operation: AuditTrailMapping.jobnoteadded(),
});
}
if (refetch) refetch();
form.resetFields();
toggleModalVisible();
notification["success"]({
message: t("notes.successes.create"),
});
insertAuditTrail({
jobid: context.jobId,
operation: AuditTrailMapping.jobnoteadded(),
});
}
};

View File

@@ -22,17 +22,9 @@ export function NotesPresetButton({ bodyshop, form }) {
};
const menu = (
<Menu
style={{
columnCount: Math.floor(bodyshop.md_notes_presets.length / 10) + 1,
}}
>
<Menu>
{bodyshop.md_notes_presets.map((i, idx) => (
<Menu.Item
onClick={() => handleSelect(i)}
key={idx}
style={{ breakInside: "avoid" }}
>
<Menu.Item onClick={() => handleSelect(i)} onItemHover key={idx}>
{i.label}
</Menu.Item>
))}

View File

@@ -14,6 +14,7 @@ export default function OwnerDetailFormComponent({ form, loading }) {
return (
<div>
<FormFieldsChanged form={form} />
<LayoutFormRow header={t("owners.forms.name")}>
<Form.Item label={t("owners.fields.ownr_title")} name="ownr_title">
<Input />
@@ -28,6 +29,7 @@ export default function OwnerDetailFormComponent({ form, loading }) {
<Input />
</Form.Item>
</LayoutFormRow>
<LayoutFormRow header={t("owners.forms.address")}>
<Form.Item label={t("owners.fields.ownr_addr1")} name="ownr_addr1">
<Input />
@@ -48,6 +50,7 @@ export default function OwnerDetailFormComponent({ form, loading }) {
<Input />
</Form.Item>
</LayoutFormRow>
<LayoutFormRow header={t("owners.forms.contact")}>
<Form.Item
label={t("owners.fields.allow_text_message")}
@@ -94,16 +97,7 @@ export default function OwnerDetailFormComponent({ form, loading }) {
>
<Input />
</Form.Item>
<Form.Item
label={t("owners.fields.tax_number")}
name="tax_number"
>
<Input />
</Form.Item>
</LayoutFormRow>
<Form.Item label={t("owners.fields.note")} name="note">
<Input.TextArea rows={4} />
</Form.Item>
</div>
);
}

View File

@@ -20,10 +20,9 @@ function OwnerDetailFormContainer({ owner, refetch }) {
if (!!result.errors) {
notification["error"]({
message: t("owners.errors.saving", {
error: JSON.stringify(result.errors),
message: JSON.stringify(result.errors),
}),
});
setLoading(false);
return;
}

View File

@@ -59,14 +59,6 @@ export default function OwnerFindModalComponent({
<PhoneFormatter>{record.ownr_ph2}</PhoneFormatter>
),
},
{
title: t("owners.fields.note"),
dataIndex: "note",
key: "note",
render: (text, record) => (
<span style={{ whiteSpace: "pre" }}>{record.note}</span>
),
},
];
const handleOnRowClick = (record) => {

View File

@@ -31,7 +31,7 @@ export default function OwnerFindModalContainer({
useEffect(() => {
if (modalProps.visible && owner) {
const s = OwnerNameDisplayFunction(owner, true);
const s = OwnerNameDisplayFunction(owner);
setSearchText(s.trim());
callSearchowners({ variables: { search: s.trim() } });

View File

@@ -12,36 +12,36 @@ export default connect(mapStateToProps, mapDispatchToProps)(OwnerNameDisplay);
export function OwnerNameDisplay({ bodyshop, ownerObject }) {
const emptyTest =
ownerObject?.ownr_fn + ownerObject?.ownr_ln + ownerObject?.ownr_co_nm;
ownerObject.ownr_fn + ownerObject.ownr_ln + ownerObject.ownr_co_nm;
if (!emptyTest || emptyTest === "null" || emptyTest.trim() === "")
return "N/A";
if (bodyshop.last_name_first)
return `${ownerObject?.ownr_ln || ""}, ${ownerObject?.ownr_fn || ""} ${
ownerObject?.ownr_co_nm || ""
return `${ownerObject.ownr_ln || ""}, ${ownerObject.ownr_fn || ""} ${
ownerObject.ownr_co_nm || ""
}`.trim();
return `${ownerObject?.ownr_fn || ""} ${ownerObject?.ownr_ln || ""} ${
return `${ownerObject.ownr_fn || ""} ${ownerObject.ownr_ln || ""} ${
ownerObject.ownr_co_nm || ""
}`.trim();
}
export function OwnerNameDisplayFunction(ownerObject, forceFirstLast = false) {
export function OwnerNameDisplayFunction(ownerObject) {
const emptyTest =
ownerObject?.ownr_fn + ownerObject?.ownr_ln + ownerObject?.ownr_co_nm;
ownerObject.ownr_fn + ownerObject.ownr_ln + ownerObject.ownr_co_nm;
if (!emptyTest || emptyTest === "null" || emptyTest.trim() === "")
return "N/A";
const rdxStore = store.getState();
if (rdxStore.user.bodyshop.last_name_first && !forceFirstLast)
return `${ownerObject?.ownr_ln || ""}, ${ownerObject?.ownr_fn || ""} ${
ownerObject?.ownr_co_nm || ""
if (rdxStore.user.bodyshop.last_name_first)
return `${ownerObject.ownr_ln || ""}, ${ownerObject.ownr_fn || ""} ${
ownerObject.ownr_co_nm || ""
}`.trim();
return `${ownerObject?.ownr_fn || ""} ${ownerObject?.ownr_ln || ""} ${
ownerObject?.ownr_co_nm || ""
return `${ownerObject.ownr_fn || ""} ${ownerObject.ownr_ln || ""} ${
ownerObject.ownr_co_nm || ""
}`.trim();
}

View File

@@ -1,66 +0,0 @@
import { useMutation } from "@apollo/client";
import { Checkbox, notification, Space, Spin } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { MUTATION_UPDATE_PO_CM_REECEIVED } from "../../graphql/parts-orders.queries";
export default function PartsOrderCmReceived({
checked,
orderLineId,
partsOrderId,
}) {
const [updateLine] = useMutation(MUTATION_UPDATE_PO_CM_REECEIVED);
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const handleChange = async (e) => {
setLoading(true);
const result = await updateLine({
variables: {
partsLineId: orderLineId,
partsOrder: { cm_received: e.target.checked },
},
update(cache) {
cache.modify({
id: cache.identify({
id: partsOrderId,
__typename: "parts_orders",
}),
fields: {
parts_order_lines(ex, { readField }) {
console.log(ex);
return ex.map((lineref) => {
if (orderLineId.id !== readField("id", lineref)) {
lineref.cm_received = e.target.checked;
}
return lineref;
});
},
},
});
},
});
if (!!!result.errors) {
notification["success"]({
message: t("parts_orders.successes.line_updated"),
});
} else {
notification["error"]({
message: t("parts_orders.errors.saving", {
error: JSON.stringify(result.errors),
}),
});
}
setLoading(false);
};
return (
<Space>
<Checkbox checked={checked} onChange={handleChange} />
{loading && <Spin size="small" />}
</Space>
);
}

View File

@@ -1,47 +0,0 @@
import React from "react";
import { Button, Popconfirm } from "antd";
import { DeleteFilled } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
import { DELETE_PARTS_ORDER_LINE } from "../../graphql/parts-orders.queries";
import { useMutation } from "@apollo/client";
export default function PartsOrderDeleteLine({
disabled,
partsLineId,
partsOrderId,
}) {
const { t } = useTranslation();
const [deletePartsOrderLine] = useMutation(DELETE_PARTS_ORDER_LINE);
return (
<Popconfirm
title={t("parts_orders.labels.confirmdelete")}
disabled={disabled}
onConfirm={async () => {
//Delete the parts return.!
await deletePartsOrderLine({
variables: { partsOrderLineId: partsLineId },
update(cache) {
cache.modify({
id: cache.identify({
__typename: "parts_orders",
id: partsOrderId,
}),
fields: {
parts_order_lines(cached, { readField }) {
return cached.filter((c) => {
return readField("id", c) !== partsLineId;
});
},
},
});
},
});
}}
>
<Button disabled={disabled}>
<DeleteFilled />
</Button>
</Popconfirm>
);
}

View File

@@ -29,8 +29,6 @@ import { alphaSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants";
import DataLabel from "../data-label/data-label.component";
import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component";
import PartsOrderDeleteLine from "../parts-order-delete-line/parts-order-delete-line.component";
import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component";
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
import PrintWrapper from "../print-wrapper/print-wrapper.component";
@@ -79,7 +77,6 @@ export function PartsOrderListTableComponent({
});
const search = queryString.parse(useLocation().search);
const selectedpartsorder = search.partsorderid;
const [searchText, setSearchText] = useState("");
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
@@ -349,23 +346,6 @@ export function PartsOrderListTableComponent({
dataIndex: "status",
key: "status",
},
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
? [
{
title: t("parts_orders.fields.cm_received"),
dataIndex: "cm_received",
key: "cm_received",
render: (text, record) => (
<PartsOrderCmReceived
orderLineId={record.id}
checked={record.cm_received}
partsorderid={selectedPartsOrderRecord.id}
/>
),
},
]
: []),
{
title: t("parts_orders.fields.backordered_on"),
dataIndex: "backordered_on",
@@ -392,21 +372,12 @@ export function PartsOrderListTableComponent({
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<Space wrap>
<PartsOrderDeleteLine
disabled={jobRO}
partsOrderStatus={record.status}
partsLineId={record.id}
partsOrderId={selectedpartsorder}
jobLineId={record.job_line_id}
/>
<PartsOrderLineBackorderButton
disabled={jobRO}
partsOrderStatus={record.status}
partsLineId={record.id}
jobLineId={record.job_line_id}
/>
</Space>
<PartsOrderLineBackorderButton
disabled={jobRO}
partsOrderStatus={record.status}
partsLineId={record.id}
jobLineId={record.job_line_id}
/>
),
},
];
@@ -432,21 +403,6 @@ export function PartsOrderListTableComponent({
);
};
const filteredPartsOrders = parts_orders
? searchText === ""
? parts_orders
: parts_orders.filter(
(b) =>
(b.order_number || "")
.toString()
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(b.vendor.name || "")
.toLowerCase()
.includes(searchText.toLowerCase())
)
: [];
return (
<Card
title={t("parts_orders.labels.parts_orders")}
@@ -457,10 +413,8 @@ export function PartsOrderListTableComponent({
</Button>
<Input.Search
placeholder={t("general.labels.search")}
value={searchText}
onChange={(e) => {
e.preventDefault();
setSearchText(e.target.value);
}}
/>
</Space>
@@ -484,7 +438,7 @@ export function PartsOrderListTableComponent({
}}
columns={columns}
rowKey="id"
dataSource={filteredPartsOrders}
dataSource={parts_orders}
onChange={handleTableChange}
/>
</Card>

View File

@@ -11,7 +11,6 @@ import {
Select,
Menu,
Dropdown,
Checkbox,
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -115,24 +114,6 @@ export function PartsOrderModalComponent({
</Space>
</Tag>
)}
{!isReturn && (
<Form.Item
name="removefrompartsqueue"
label={t("parts_orders.labels.removefrompartsqueue")}
valuePropName="checked"
>
<Checkbox />
</Form.Item>
)}
{OEConnection.treatment === "on" && !isReturn && (
<Form.Item
name="is_quote"
label={t("parts_orders.labels.is_quote")}
valuePropName="checked"
>
<Checkbox />
</Form.Item>
)}
</LayoutFormRow>
<Divider orientation="left">
{t("parts_orders.labels.inthisorder")}
@@ -299,33 +280,17 @@ export function PartsOrderModalComponent({
>
<Input.TextArea rows={3} />
</Form.Item>
<Form.Item noStyle shouldUpdate>
{() => {
const is_quote = form.getFieldValue("is_quote");
if (is_quote) setSendType("oec");
return (
<Radio.Group
defaultValue={sendType}
value={sendType}
onChange={(e) => setSendType(e.target.value)}
>
<Radio disabled={is_quote} value={"none"}>
{t("general.labels.none")}
</Radio>
<Radio disabled={is_quote} value={"e"}>
{t("parts_orders.labels.email")}
</Radio>
<Radio disabled={is_quote} value={"p"}>
{t("parts_orders.labels.print")}
</Radio>
{OEConnection.treatment === "on" && !isReturn && (
<Radio value={"oec"}>{t("parts_orders.labels.oec")}</Radio>
)}
</Radio.Group>
);
}}
</Form.Item>
<Radio.Group
defaultValue={sendType}
onChange={(e) => setSendType(e.target.value)}
>
<Radio value={"none"}>{t("general.labels.none")}</Radio>
<Radio value={"e"}>{t("parts_orders.labels.email")}</Radio>
<Radio value={"p"}>{t("parts_orders.labels.print")}</Radio>
{OEConnection.treatment === "on" && !isReturn && (
<Radio value={"oec"}>{t("parts_orders.labels.oec")}</Radio>
)}
</Radio.Group>
</div>
);
}

View File

@@ -32,7 +32,6 @@ import PartsOrderModalComponent from "./parts-order-modal.component";
import axios from "axios";
import { useTreatments } from "@splitsoftware/splitio-react";
import _ from "lodash";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
@@ -91,58 +90,32 @@ export function PartsOrderModalContainer({
const [insertPartOrder] = useMutation(INSERT_NEW_PARTS_ORDERS);
const [updateJobLines] = useMutation(UPDATE_JOB_LINE_STATUS);
const [updateJob] = useMutation(UPDATE_JOB);
const handleFinish = async ({
removefrompartsqueue,
is_quote,
...values
}) => {
const handleFinish = async (values) => {
logImEXEvent("parts_order_insert");
setSaving(true);
let insertResult;
insertResult = await insertPartOrder({
variables: {
po: [
{
...values,
order_date: moment().format("YYYY-MM-DD"),
orderedby: currentUser.email,
jobid: jobId,
user_email: currentUser.email,
return: isReturn,
status: is_quote
? bodyshop.md_order_statuses.default_quote || "Quote"
: bodyshop.md_order_statuses.default_ordered || "Ordered*",
},
],
},
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"],
const insertResult = await insertPartOrder({
variables: {
po: [
{
...values,
orderedby: currentUser.email,
jobid: jobId,
user_email: currentUser.email,
return: isReturn,
status: bodyshop.md_order_statuses.default_ordered || "Ordered*",
},
],
},
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"],
});
if (!!insertResult.error) {
notification["error"]({
message: t("parts_orders.errors.creating"),
description: JSON.stringify(insertResult.error),
});
if (!!insertResult.errors) {
notification["error"]({
message: t("parts_orders.errors.creating"),
description: JSON.stringify(insertResult.errors),
});
return;
}
notification["success"]({
message: values.isReturn
? t("parts_orders.successes.return_created")
: t("parts_orders.successes.created"),
});
insertAuditTrail({
jobid: jobId,
operation: isReturn
? AuditTrailMapping.jobspartsreturn(
insertResult.data.insert_parts_orders.returning[0].order_number
)
: AuditTrailMapping.jobspartsorder(
insertResult.data.insert_parts_orders.returning[0].order_number
),
});
return;
}
const jobLinesResult = await updateJobLines({
variables: {
@@ -151,22 +124,20 @@ export function PartsOrderModalContainer({
.map((item) => item.job_line_id),
status: isReturn
? bodyshop.md_order_statuses.default_returned || "Returned*"
: is_quote
? bodyshop.md_order_statuses.default_quote || "Quote"
: bodyshop.md_order_statuses.default_ordered || "Ordered*",
},
});
if (!isReturn && removefrompartsqueue) {
await updateJob({
variables: {
jobId: jobId,
job: {
queued_for_parts: false,
},
},
});
}
insertAuditTrail({
jobid: jobId,
operation: isReturn
? AuditTrailMapping.jobspartsreturn(
insertResult.data.insert_parts_orders.returning[0].order_number
)
: AuditTrailMapping.jobspartsorder(
insertResult.data.insert_parts_orders.returning[0].order_number
),
});
if (!!jobLinesResult.errors) {
notification["error"]({
@@ -175,6 +146,12 @@ export function PartsOrderModalContainer({
});
}
notification["success"]({
message: values.isReturn
? t("parts_orders.successes.return_created")
: t("parts_orders.successes.created"),
});
if (values.vendorid === bodyshop.inhousevendorid) {
setBillEnterContext({
actions: { refetch: refetch },
@@ -328,7 +305,6 @@ export function PartsOrderModalContainer({
quantity: value.part_qty,
job_line_id: isReturn ? value.joblineid : value.id,
part_type: value.part_type,
...(isReturn && { cm_received: false }),
});
return acc;
}, [])

Some files were not shown because too many files have changed in this diff Show More