Compare commits

...

84 Commits

Author SHA1 Message Date
Patrick Fic
f5e20b7041 IO-1675 Add OEM Part No to parts order modal. 2022-01-27 15:18:52 -08:00
Patrick Fic
8d1988d4ad IO-1674 Resolve new bill lines issues. 2022-01-27 15:07:00 -08:00
Patrick Fic
cd071500cf IO-1682 Restore customer name for payments. 2022-01-27 14:40:29 -08:00
Patrick Fic
65d93b8f9b IO-1669 Add print production by filtered category 2022-01-27 14:34:06 -08:00
Patrick Fic
b147d4c2ed IO-1664 Remove extra spaces for DMS story. 2022-01-27 14:13:49 -08:00
Patrick Fic
0c8c303d08 IO-1656 Allow images only to be sent via messaging. 2022-01-27 14:02:09 -08:00
Patrick Fic
d83e2ace2e IO-1671 Improved date handling. 2022-01-27 11:08:51 -08:00
Patrick Fic
468227b7b9 IO-1659 Resolve date saving issue. 2022-01-27 10:47:43 -08:00
Patrick Fic
021098fa2a IO-1674 Improve bill posting line display & order by price descending. 2022-01-27 08:46:19 -08:00
Patrick Fic
49f5668e89 IO-1674 Reorder bill posting lines by price. 2022-01-27 08:41:37 -08:00
Patrick Fic
9b444a130b IO-1673 Posting bills to removed lines for credit memos. 2022-01-27 08:37:26 -08:00
Patrick Fic
25fd90f881 IO-1672 Updated vendor discount display. 2022-01-26 14:06:24 -08:00
Patrick Fic
aa61aa6702 IO-1672 Updated bill line discount display. 2022-01-26 14:03:28 -08:00
Patrick Fic
a8b1537cd6 IO-1671 Improved date handling for form item. 2022-01-26 13:26:42 -08:00
Patrick Fic
2c702da1fd IO-1664 IO-1670 Improve DMS Story. 2022-01-26 13:26:28 -08:00
Patrick Fic
cb48ea64f9 IO-1682 Remove name from Payable/Payments exporting. 2022-01-26 12:34:43 -08:00
Patrick Fic
c17e1e92aa IO-1691 Segmented logrocket tracking. 2022-01-26 12:32:08 -08:00
Patrick Fic
b546d90c9e IO-1651 add estimates written/converted. 2022-01-26 08:46:44 -08:00
Patrick Fic
3985293cee IO-1656 Send media through sms fix. 2022-01-26 08:44:44 -08:00
Patrick Fic
4bd3b851ef IO-1657 Add status to global search. 2022-01-24 16:30:10 -08:00
Patrick Fic
bef76491d7 IO-1630 Add count to Visual Board. 2022-01-24 16:22:57 -08:00
Patrick Fic
4f3090c3bd IO-1377 Vendor name consistency on drawer title. 2022-01-24 16:07:08 -08:00
Patrick Fic
7ddd29ca27 IO-1642 Resolve delivery invalid date on intake. 2022-01-24 13:44:02 -08:00
Patrick Fic
ab607e9919 Merge branch 'release/2021-12-31' into test 2022-01-24 13:33:35 -08:00
Patrick Fic
5511dd44ce Autohouse - Export 1 2022-01-24 13:32:38 -08:00
Patrick Fic
d6a84b8b7f Remove autohouse testing. 2022-01-24 10:53:41 -08:00
Patrick Fic
c962d848c1 PBS Improvements. 2022-01-21 14:34:15 -08:00
Patrick Fic
febcff3383 Merged in release/2022-01-21 (pull request #354)
Add PBS Logging.
2022-01-21 18:36:56 +00:00
Patrick Fic
61090aa04c Add PBS Logging. 2022-01-21 10:32:02 -08:00
Patrick Fic
e0f75fa357 Merged in release/2022-01-21 (pull request #353)
release/2022-01-21

Approved-by: Patrick Fic
2022-01-21 00:20:46 +00:00
Patrick Fic
9aa85b3ab5 IO-1552 AH Updates. 2022-01-20 16:20:09 -08:00
Patrick Fic
4704fd9ce1 IO-1573 Resolve CDK job costing error. 2022-01-20 15:04:57 -08:00
Patrick Fic
a0f06ffdc2 IO-1558 Add past due indicator to Kanban card. 2022-01-20 13:10:45 -08:00
Patrick Fic
c4eed109e9 Merged in release/2022-01-21 (pull request #352)
Release/2022 01 21
2022-01-20 18:38:19 +00:00
Patrick Fic
7d9eb737ec IO-1552 Autohouse include additional fields. 2022-01-20 09:54:48 -08:00
Patrick Fic
6fbf954dc2 IO-1652 Add production filtered by technician 2022-01-20 08:27:38 -08:00
Patrick Fic
968da48399 IO-1654 add discount for 900500 lines. 2022-01-20 08:12:12 -08:00
Patrick Fic
83906ea788 Merged in release/2022-01-21 (pull request #351)
Release/2022 01 21
2022-01-20 00:30:05 +00:00
Patrick Fic
83255cd316 IO-1653 Include Other images when downloading 2022-01-19 16:12:16 -08:00
Patrick Fic
1966e91463 IO-1644 Allow invoice date to be set on job closure. 2022-01-19 14:09:24 -08:00
Patrick Fic
413400fa71 IO-1652 Printing tickets from tech console. 2022-01-19 12:45:16 -08:00
Patrick Fic
abf9d0b7f4 IO-1558 2022-01-19 12:03:30 -08:00
Patrick Fic
4deed41a12 Revert "IO-1606 Include additional costs in reconciliation."
This reverts commit 821b044515.
2022-01-19 11:50:14 -08:00
Patrick Fic
4a7eb9b373 User email case sensitivity on login. 2022-01-19 11:46:49 -08:00
Patrick Fic
bf81c6dd06 ARMS Updates 2022-01-19 08:38:03 -08:00
Patrick Fic
7a89781a20 Merged in release/2022-01-21 (pull request #350)
Release/2022 01 21
2022-01-17 21:21:33 +00:00
Patrick Fic
34ae2c56b7 IO-1642 Resolve delivery checklist not including dates. 2022-01-17 12:35:43 -08:00
Patrick Fic
e2941bfe84 IO-1558 Add color to dates that are today. 2022-01-17 12:08:09 -08:00
Patrick Fic
821b044515 IO-1606 Include additional costs in reconciliation. 2022-01-17 11:48:12 -08:00
Patrick Fic
933e0a62fb IO-1573 CDK Job costing updates 2022-01-17 10:18:07 -08:00
Patrick Fic
25bc343027 IO-1643 Add shop name to bread crumb. 2022-01-17 09:26:38 -08:00
Patrick Fic
77727cc4b1 Merged in release/2022-01-14 (pull request #348)
release/2022-01-14

Approved-by: Patrick Fic
2022-01-13 21:43:19 +00:00
Patrick Fic
002301c792 Enable autohouse upload again. 2022-01-13 13:42:54 -08:00
Patrick Fic
7ee544b013 Minor PBS Updates. 2022-01-13 10:55:16 -08:00
Patrick Fic
3bc1785351 IO-1627 Add production category report. 2022-01-13 09:19:41 -08:00
Patrick Fic
1f58ee7d67 Merged in release/2022-01-14 (pull request #347)
release/2022-01-14

Approved-by: Patrick Fic
2022-01-12 21:44:45 +00:00
Patrick Fic
5ec1e84671 IO-1627 Add category to production. 2022-01-12 12:49:38 -08:00
Patrick Fic
d9fa00e633 IO-1636 Resolve scheduling error 2022-01-12 12:21:30 -08:00
Patrick Fic
53a55c2b6e Merged in release/2022-01-14 (pull request #345)
Release/2022 01 14
2022-01-12 16:24:17 +00:00
Patrick Fic
5fe8a15a8c IO-1622 Resolve shift clock. 2022-01-12 08:15:16 -08:00
Patrick Fic
70cfbf2f5b IO-1552 Autohouse schema updates. 2022-01-11 13:13:54 -08:00
Patrick Fic
5e6ab55159 Merged in release/2022-01-14 (pull request #343)
Tech Console Posting
2022-01-11 20:28:57 +00:00
Patrick Fic
2aa6fdfac5 IO-1622 Add Rates to tech console. 2022-01-11 12:28:15 -08:00
Patrick Fic
8acd51c4f1 Merged in release/2022-01-14 (pull request #342)
Add RBAC to employees page.
2022-01-11 20:03:49 +00:00
Patrick Fic
e1d5558dac Add RBAC to employees page. 2022-01-11 12:03:09 -08:00
Patrick Fic
061212f915 Merge branch 'master' into release/2022-01-14 2022-01-11 12:01:03 -08:00
Patrick Fic
14bb735f00 Merged in release/2022-01-14 (pull request #341)
IO-1632 Resolve missing cieca code on clock out.
2022-01-11 19:40:43 +00:00
Patrick Fic
9714371a36 IO-1632 Resolve missing cieca code on clock out. 2022-01-11 11:39:56 -08:00
Patrick Fic
c7c5feeabe Merged in release/2022-01-14 (pull request #340)
IO-1632 Update tech console for CDK posting.
2022-01-11 18:17:39 +00:00
Patrick Fic
1c2a64cf50 IO-1632 Update tech console for CDK posting. 2022-01-11 10:12:10 -08:00
Patrick Fic
7d4d97ce4e Merged in release/2022-01-07 (pull request #339)
Release/2022 01 07
2022-01-08 00:43:51 +00:00
Patrick Fic
1d70053459 Merged in release/2022-01-07 (pull request #338)
IO-1612 Disable popover on PTO

Approved-by: Patrick Fic
2022-01-07 18:25:38 +00:00
Patrick Fic
675ccff3ce Merged in release/2022-01-07 (pull request #337)
release/2022-01-07

Approved-by: Patrick Fic
2022-01-07 18:01:34 +00:00
Patrick Fic
25b236072e Merged in release/2022-01-07 (pull request #336)
release/2022-01-07

Approved-by: Patrick Fic
2022-01-06 22:26:49 +00:00
Patrick Fic
b22318015e Merged in release/2022-01-07 (pull request #335)
release/2022-01-07

Approved-by: Patrick Fic
2022-01-06 18:14:27 +00:00
Patrick Fic
ddb7d8915e Merged in release/2022-01-07 (pull request #334)
release/2022-01-07

Approved-by: Patrick Fic
2022-01-04 20:33:41 +00:00
Patrick Fic
bca8ce0898 Merged in release/2022-01-07 (pull request #333)
release/2022-01-07

Approved-by: Patrick Fic
2022-01-03 18:49:44 +00:00
Patrick Fic
4fff9a5c99 Merged in hotfix/2021-12-29 (pull request #329)
hotfix/2021-12-29

Approved-by: Patrick Fic
2021-12-30 05:55:45 +00:00
Patrick Fic
e98e3049d8 Merged in hotfix/2021-12-29 (pull request #327)
hotfix/2021-12-29

Approved-by: Patrick Fic
2021-12-30 04:06:19 +00:00
Patrick Fic
456b00a605 Merged in release/2021-12/31 (pull request #326)
release/2021-12/31

Approved-by: Patrick Fic
2021-12-29 22:24:33 +00:00
Patrick Fic
cc078df80d Merged in release/2021-12/31 (pull request #325)
release/2021-12/31

Approved-by: Patrick Fic
2021-12-29 16:27:26 +00:00
Patrick Fic
80582692cf Merged in hotfix/2021-12-28 (pull request #323)
hotfix/2021-12-28

Approved-by: Patrick Fic
2021-12-28 22:17:06 +00:00
Patrick Fic
1bb0cbaebc Merged in release/2021-12/31 (pull request #321)
release/2021-12/31

Approved-by: Patrick Fic
2021-12-28 19:04:38 +00:00
Patrick Fic
a20a01dbd1 Merged in release/2021-12/31 (pull request #320)
release/2021-12/31

Approved-by: Patrick Fic
2021-12-27 22:00:14 +00:00
58 changed files with 1504 additions and 494 deletions

View File

@@ -10,7 +10,7 @@ npx hasura migrate apply --endpoint https://db.imex.online/ --admin-secret 'Prod
npx hasura migrate apply --endpoint https://db.test.bodyshop.app/ --admin-secret 'Test-ImEXOnlineBySnaptSoftware!'
NGROK TEsting:
./ngrok.exe http http://localhost:5000 -host-header="localhost:5000"
./ngrok.exe http http://localhost:4000 -host-header="localhost:4000"
Finding deadfiles - run from client directory
npx deadfile ./src/index.js --exclude build templates

View File

@@ -1,4 +1,4 @@
<babeledit_project be_version="2.7.1" version="1.2">
<babeledit_project version="1.2" be_version="2.7.1">
<!--
BabelEdit project file
@@ -25448,6 +25448,27 @@
<folder_node>
<name>dms</name>
<children>
<concept_node>
<name>damageto</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>defaultstory</name>
<definition_loaded>false</definition_loaded>
@@ -25469,6 +25490,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>invoicedatefuture</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>kmoutnotgreaterthankmin</name>
<definition_loaded>false</definition_loaded>
@@ -37146,6 +37188,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>estimates_written_converted</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>estimator_detail</name>
<definition_loaded>false</definition_loaded>
@@ -38007,6 +38070,48 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>production_by_category</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>production_by_category_one</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>production_by_csr</name>
<definition_loaded>false</definition_loaded>
@@ -38133,6 +38238,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>production_by_technician_one</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>purchases_by_cost_center_detail</name>
<definition_loaded>false</definition_loaded>

View File

@@ -2,7 +2,6 @@ import { ApolloProvider } from "@apollo/client";
import { SplitFactory, SplitSdk } from "@splitsoftware/splitio-react";
import { ConfigProvider } from "antd";
import enLocale from "antd/es/locale/en_US";
import LogRocket from "logrocket";
import moment from "moment";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -10,13 +9,8 @@ import GlobalLoadingBar from "../components/global-loading-bar/global-loading-ba
import client from "../utils/GraphQLClient";
import App from "./App";
moment.locale("en-US");
//tracker.start();
if (process.env.NODE_ENV === "production") LogRocket.init("gvfvfw/bodyshopapp");
export const factory = SplitSdk({
core: {
authorizationKey: process.env.REACT_APP_SPLIT_API,

View File

@@ -1,4 +1,6 @@
import { useTreatments } from "@splitsoftware/splitio-react";
import { Button, Result } from "antd";
import LogRocket from "logrocket";
import React, { lazy, Suspense, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -9,15 +11,18 @@ import ErrorBoundary from "../components/error-boundary/error-boundary.component
//Component Imports
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
import DisclaimerPage from "../pages/disclaimer/disclaimer.page";
import LandingPage from "../pages/landing/landing.page";
import TechPageContainer from "../pages/tech/tech.page.container";
import { setOnline } from "../redux/application/application.actions";
import { selectOnline } from "../redux/application/application.selectors";
import { checkUserSession } from "../redux/user/user.actions";
import { selectCurrentUser } from "../redux/user/user.selectors";
import {
selectBodyshop,
selectCurrentUser,
} from "../redux/user/user.selectors";
import PrivateRoute from "../utils/private-route";
import "./App.styles.scss";
import LandingPage from "../pages/landing/landing.page";
const ResetPassword = lazy(() =>
import("../pages/reset-password/reset-password.component")
);
@@ -32,13 +37,26 @@ const MobilePaymentContainer = lazy(() =>
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
online: selectOnline,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
checkUserSession: () => dispatch(checkUserSession()),
setOnline: (isOnline) => dispatch(setOnline(isOnline)),
});
export function App({ checkUserSession, currentUser, online, setOnline }) {
export function App({
bodyshop,
checkUserSession,
currentUser,
online,
setOnline,
}) {
const { LogRocket_Tracking } = useTreatments(
["LogRocket_Tracking"],
{},
bodyshop && bodyshop.imexshopid
);
useEffect(() => {
if (!navigator.onLine) {
setOnline(false);
@@ -59,6 +77,16 @@ export function App({ checkUserSession, currentUser, online, setOnline }) {
window.addEventListener("online", function (e) {
setOnline(true);
});
useEffect(() => {
if (currentUser.authorized) {
if (
process.env.NODE_ENV === "production" &&
LogRocket_Tracking.treatment === "on"
) {
LogRocket.init("gvfvfw/bodyshopapp");
}
}
}, [currentUser.authorized, LogRocket_Tracking.treatment]);
if (currentUser.authorized === null) {
return <LoadingSpinner message={t("general.labels.loggingin")} />;

View File

@@ -91,13 +91,13 @@
color: blue;
}
.production-completion-1 {
animation: production-completion-1-blinker 5s linear infinite;
.production-completion-soon {
color: rgba(255, 140, 0, 0.8);
font-weight: bold;
}
@keyframes production-completion-1-blinker {
50% {
background: rgba(207, 12, 12, 0.555);
}
.production-completion-past {
color: rgba(255, 0, 0, 0.8);
font-weight: bold;
}
.react-resizable {

View File

@@ -245,7 +245,7 @@ function BillEnterModalContainer({
return (
<Modal
title={t("bills.labels.new")}
width={"90%"}
width={"98%"}
visible={billEnterModal.visible}
okText={t("general.actions.save")}
keyboard="false"

View File

@@ -1,4 +1,4 @@
import { DeleteFilled, WarningOutlined } from "@ant-design/icons";
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
import {
Button,
Form,
@@ -8,6 +8,7 @@ import {
Space,
Switch,
Table,
Tooltip,
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -43,7 +44,7 @@ export function BillEnterModalLinesComponent({
title: t("billlines.fields.jobline"),
dataIndex: "joblineid",
editable: true,
width: "20rem",
formItemProps: (field) => {
return {
key: `${field.index}joblinename`,
@@ -57,11 +58,24 @@ export function BillEnterModalLinesComponent({
],
};
},
wrapper: (props) => (
<Form.Item
noStyle
shouldUpdate={(prev, cur) =>
prev.is_credit_memo !== cur.is_credit_memo
}
>
{() => {
return props.children;
}}
</Form.Item>
),
formInput: (record, index) => (
<BillLineSearchSelect
disabled={disabled}
options={lineData}
style={{ width: "100%", minWidth: "10rem" }}
allowRemoved={form.getFieldValue("is_credit_memo") || false}
onSelect={(value, opt) => {
setFieldsValue({
billlines: getFieldsValue(["billlines"]).billlines.map(
@@ -201,23 +215,58 @@ export function BillEnterModalLinesComponent({
};
},
formInput: (record, index) => (
<CurrencyInput min={0} disabled={disabled} />
<CurrencyInput
min={0}
disabled={disabled}
controls={false}
addonAfter={
<Form.Item shouldUpdate noStyle>
{() => {
const line = getFieldsValue(["billlines"]).billlines[index];
if (!!!line) return null;
let lineDiscount = 1 - line.actual_cost / line.actual_price;
if (isNaN(lineDiscount)) lineDiscount = 0;
return (
<Tooltip title={`${(lineDiscount * 100).toFixed(2) || 0}%`}>
<DollarCircleFilled
style={{
color:
Math.abs(lineDiscount - discount) > 0.005
? lineDiscount > discount
? "orange"
: "red"
: "green",
}}
/>
</Tooltip>
);
}}
</Form.Item>
}
/>
),
additional: (record, index) => (
<Form.Item shouldUpdate>
{() => {
const line = getFieldsValue(["billlines"]).billlines[index];
if (!!!line) return null;
const lineDiscount = (
1 -
Math.round((line.actual_cost / line.actual_price) * 100) / 100
).toPrecision(2);
// additional: (record, index) => (
// <Form.Item shouldUpdate>
// {() => {
// const line = getFieldsValue(["billlines"]).billlines[index];
// if (!!!line) return null;
// const lineDiscount = (
// 1 -
// Math.round((line.actual_cost / line.actual_price) * 100) / 100
// ).toPrecision(2);
if (lineDiscount - discount === 0) return <div />;
return <WarningOutlined style={{ color: "red" }} />;
}}
</Form.Item>
),
// return (
// <Tooltip title={`${(lineDiscount * 100).toFixed(0) || 0}%`}>
// <DollarCircleFilled
// style={{
// color: lineDiscount - discount !== 0 ? "red" : "green",
// }}
// />
// </Tooltip>
// );
// }}
// </Form.Item>
// ),
},
{
title: t("billlines.fields.cost_center"),
@@ -488,6 +537,7 @@ const EditableCell = ({
formInput,
formItemProps,
additional,
wrapper,
...restProps
}) => {
if (additional)
@@ -505,7 +555,20 @@ const EditableCell = ({
</Space>
</td>
);
if (wrapper)
return (
<wrapper>
<td {...restProps}>
<Form.Item
labelCol={{ span: 0 }}
name={dataIndex}
{...(formItemProps && formItemProps(record))}
>
{(formInput && formInput(record, record.name)) || children}
</Form.Item>
</td>
</wrapper>
);
return (
<td {...restProps}>
<Form.Item

View File

@@ -4,7 +4,10 @@ import { useTranslation } from "react-i18next";
//To be used as a form element only.
const { Option } = Select;
const BillLineSearchSelect = ({ options, disabled, ...restProps }, ref) => {
const BillLineSearchSelect = (
{ options, disabled, allowRemoved, ...restProps },
ref
) => {
const { t } = useTranslation();
return (
@@ -12,6 +15,7 @@ const BillLineSearchSelect = ({ options, disabled, ...restProps }, ref) => {
disabled={disabled}
ref={ref}
showSearch
dropdownMatchSelectWidth={false}
// optionFilterProp="line_desc"
filterOption={(inputValue, option) => {
return (
@@ -36,7 +40,7 @@ const BillLineSearchSelect = ({ options, disabled, ...restProps }, ref) => {
{options
? options.map((item) => (
<Option
disabled={item.removed}
disabled={allowRemoved ? false : item.removed}
key={item.id}
value={item.id}
cost={item.act_price ? item.act_price : 0}
@@ -49,9 +53,14 @@ const BillLineSearchSelect = ({ options, disabled, ...restProps }, ref) => {
...(item.removed ? { textDecoration: "line-through" } : {}),
}}
>
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
<span>{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
item.oem_partno ? ` - ${item.oem_partno}` : ""
}${item.act_price ? ` - $${item.act_price}` : ``}`}
}`}</span>
<span style={{ float: "right", paddingleft: "1rem" }}>
{item.act_price
? `$${item.act_price && item.act_price.toFixed(2)}`
: ``}
</span>
</Option>
))
: null}

View File

@@ -58,7 +58,8 @@ export function BillsListTableComponent({
disabled={
record.is_credit_memo || record.vendorid === bodyshop.inhousevendorid
}
onClick={() =>
onClick={() => {
console.log(record);
setPartsOrderContext({
actions: {},
context: {
@@ -74,12 +75,13 @@ export function BillsListTableComponent({
cost: i.actual_cost,
quantity: i.quantity,
joblineid: i.joblineid,
oem_partno: i.jobline && i.jobline.oem_partno,
};
}),
isReturn: true,
},
})
}
});
}}
>
{t("bills.actions.return")}
</Button>

View File

@@ -5,21 +5,25 @@ import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { selectBreadcrumbs } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import GlobalSearch from "../global-search/global-search.component";
import "./breadcrumbs.styles.scss";
const mapStateToProps = createStructuredSelector({
breadcrumbs: selectBreadcrumbs,
bodyshop: selectBodyshop,
});
export function BreadCrumbs({ breadcrumbs }) {
export function BreadCrumbs({ breadcrumbs, bodyshop }) {
return (
<Row className="breadcrumb-container">
<Col xs={24} sm={24} md={16}>
<Breadcrumb separator=">">
<Breadcrumb.Item>
<Link to={`/manage`}>
<HomeFilled />
<HomeFilled />{" "}
{(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) ||
""}
</Link>
</Breadcrumb.Item>
{breadcrumbs.map((item) =>

View File

@@ -45,13 +45,14 @@ function ChatSendMessageComponent({
const { t } = useTranslation();
const handleEnter = () => {
if (message === "" || !message) return;
logImEXEvent("messaging_send_message");
const selectedImages = selectedMedia.filter((i) => i.isSelected);
if (selectedImages < 11) {
if ((message === "" || !message) && selectedImages.length === 0) return;
logImEXEvent("messaging_send_message");
if (selectedImages.length < 11) {
sendMessage({
to: conversation.phone_num,
body: message,
body: message || "",
messagingServiceSid: bodyshop.messagingservicesid,
conversationid: conversation.id,
selectedMedia: selectedImages,
@@ -92,7 +93,7 @@ function ChatSendMessageComponent({
</span>
<SendOutlined
className="imex-flex-row__margin"
disabled={message === "" || !message}
// disabled={message === "" || !message}
onClick={handleEnter}
/>
<Spin

View File

@@ -2,7 +2,6 @@ import { DeleteFilled, DownOutlined } from "@ant-design/icons";
import {
Button,
Card,
DatePicker,
Divider,
Dropdown,
Form,
@@ -15,6 +14,7 @@ import {
Typography,
} from "antd";
import Dinero from "dinero.js";
import moment from "moment";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -23,9 +23,9 @@ import { determineDmsType } from "../../pages/dms/dms.container";
import { selectBodyshop } from "../../redux/user/user.selectors";
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import moment from "moment";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -80,11 +80,25 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
layout="vertical"
onFinish={handleFinish}
initialValues={{
story: t("jobs.labels.dms.defaultstory", {
story: `${t("jobs.labels.dms.defaultstory", {
ro_number: job.ro_number,
area_of_damage:
(job.area_of_damage && job.area_of_damage.impact1) || "UNKNOWN",
}).substr(0, 239),
ownr_nm: `${job.ownr_fn || ""} ${job.ownr_ln || ""} ${
job.ownr_co_nm || ""
}`.trim(),
ins_co_nm: job.ins_co_nm || "N/A",
clm_po: `${job.clm_no ? `${job.clm_no} ` : ""}${
job.po_number || ""
}`,
}).trim()}.${
job.area_of_damage && job.area_of_damage.impact1
? " " +
t("jobs.labels.dms.damageto", {
area_of_damage:
(job.area_of_damage && job.area_of_damage.impact1) ||
"UNKNOWN",
})
: ""
}`.substr(0, 239),
inservicedate: moment("2019-01-01"),
}}
>
@@ -162,7 +176,7 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
name="inservicedate"
label={t("jobs.fields.dms.inservicedate")}
>
<DatePicker format="MM/DD/YYYY" />
<FormDatePicker />
</Form.Item>
</LayoutFormRow>
<Space>

View File

@@ -1,14 +1,19 @@
import { DatePicker } from "antd";
import moment from "moment";
import React, { forwardRef } from "react";
import React, { useRef } from "react";
//To be used as a form element only.
const dateFormat = "MM/DD/YYYY";
const FormDatePicker = (
{ value, onChange, onBlur, onlyFuture, ...restProps },
ref
) => {
export default function FormDatePicker({
value,
onChange,
onBlur,
onlyFuture,
...restProps
}) {
const ref = useRef();
const handleChange = (newDate) => {
if (value !== newDate && onChange) {
onChange(newDate);
@@ -19,17 +24,44 @@ const FormDatePicker = (
if (e.key.toLowerCase() === "t") {
if (onChange) {
onChange(new moment());
// if (ref.current && ref.current.blur) ref.current.blur();
}
} else if (e.key.toLowerCase() === "enter") {
if (ref.current && ref.current.blur) ref.current.blur();
}
};
const handleBlur = (e) => {
const v = e.target.value;
if (!v) return;
const _a = moment(
v,
["MMDDYY", "MMDDYYYY", "MMDD", "MM/DD/YY"],
"en",
false
);
if (_a.isValid() && value && value.isValid && value.isValid()) {
_a.set({
hours: value.hours(),
minutes: value.minutes(),
seconds: value.seconds(),
milliseconds: value.milliseconds(),
});
}
if (_a.isValid() && onChange) onChange(_a);
};
return (
<div onKeyDown={handleKeyDown}>
<DatePicker
ref={ref}
value={value ? moment(value) : null}
onChange={handleChange}
format={dateFormat}
onBlur={onBlur}
onBlur={onBlur || handleBlur}
disabledTime
{...(onlyFuture && {
disabledDate: (d) => moment().subtract(1, "day").isAfter(d),
@@ -38,6 +70,4 @@ const FormDatePicker = (
/>
</div>
);
};
export default forwardRef(FormDatePicker);
}

View File

@@ -38,6 +38,7 @@ export default function GlobalSearch() {
<Link to={`/manage/jobs/${job.id}`}>
<Space size="small" split={<Divider type="vertical" />}>
<strong>{job.ro_number || t("general.labels.na")}</strong>
<span>{`${job.status || ""}`}</span>
<span>{`${job.ownr_fn || ""} ${job.ownr_ln || ""} ${
job.ownr_co_nm || ""
}`}</span>

View File

@@ -105,6 +105,10 @@ export function JobChecklistForm({
completed_at: new Date(),
},
...(type === "intake" &&
values.scheduled_delivery && {
scheduled_delivery: values.scheduled_delivery,
}),
...(type === "deliver" && {
scheduled_delivery: values.scheduled_delivery,
actual_delivery: values.actual_delivery,
@@ -171,7 +175,12 @@ export function JobChecklistForm({
});
}
};
console.log(job, {
removeFromProduction: true,
actual_completion:
job && job.actual_completion && moment(job.actual_completion),
actual_delivery: job && job.actual_delivery && moment(job.actual_delivery),
});
return (
<Card
title={t("checklist.labels.checklist")}
@@ -195,21 +204,27 @@ export function JobChecklistForm({
addToProduction: true,
allow_text_message: job.owner && job.owner.allow_text_message,
scheduled_completion:
(job && job.scheduled_completion) ||
(job.labbrs && job.larhrs
? moment().businessAdd(
(job.labhrs.aggregate.sum.mod_lb_hrs +
job.larhrs.aggregate.sum.mod_lb_hrs) /
bodyshop.target_touchtime,
"days"
)
: null),
scheduled_delivery: job && job.scheduled_delivery,
(job &&
job.scheduled_completion &&
moment(job.scheduled_completion)) ||
(job &&
job.labhrs &&
job.larhrs &&
moment().businessAdd(
(job.labhrs.aggregate.sum.mod_lb_hrs ||
0 + job.larhrs.aggregate.sum.mod_lb_hrs ||
0) / bodyshop.target_touchtime,
"days"
)),
scheduled_delivery:
job.scheduled_delivery && moment(job.scheduled_delivery),
}),
...(type === "deliver" && {
removeFromProduction: true,
actual_completion: job && job.actual_completion,
actual_delivery: job && job.actual_delivery,
actual_completion:
job && job.actual_completion && moment(job.actual_completion),
actual_delivery:
job && job.actual_delivery && moment(job.actual_delivery),
}),
...formItems
.filter((fi) => fi.value)

View File

@@ -1,11 +1,13 @@
import { useMutation } from "@apollo/client";
import { Button, Form, notification, DatePicker } from "antd";
import { Button, Form, notification } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import moment from "moment";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
export default function JobsAdminDatesChange({ job }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
@@ -54,7 +56,7 @@ export default function JobsAdminDatesChange({ job }) {
label={t("jobs.fields.date_estimated")}
name="date_estimated"
>
<DatePicker format="MM/DD/YYYY" />
<FormDatePicker format="MM/DD/YYYY" />
</Form.Item>
<Form.Item label={t("jobs.fields.date_open")} name="date_open">
<DateTimePicker />

View File

@@ -1,4 +1,4 @@
import { DatePicker, Form, Statistic, Tooltip } from "antd";
import { Form, Statistic, Tooltip } from "antd";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -34,7 +34,7 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
label={t("jobs.fields.date_estimated")}
name="date_estimated"
>
<DatePicker disabled={jobRO} format="MM/DD/YYYY" />
<FormDatePicker disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.date_open")} name="date_open">
<DateTimePicker disabled={jobRO} />

View File

@@ -36,7 +36,7 @@ export function JobsDocumentsDownloadButton({
);
const imagesToDownload = [
...galleryImages.images.filter((image) => image.isSelected),
// ...galleryImages.other.filter((image) => image.isSelected),
...galleryImages.other.filter((image) => image.isSelected),
];
function downloadProgress(progressEvent) {
@@ -123,6 +123,7 @@ export function JobsDocumentsDownloadButton({
a.click();
}
};
return (
<>
<Button

View File

@@ -91,68 +91,76 @@ export function PartsOrderModalComponent({
<div>
{fields.map((field, index) => (
<Form.Item required={false} key={field.key}>
<LayoutFormRow grow noDivider>
<Form.Item
//span={8}
label={t("parts_orders.fields.line_desc")}
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("parts_orders.fields.line_remarks")}
key={`${index}line_remarks`}
name={[field.name, "line_remarks"]}
>
<Input />
</Form.Item>
{
// <Form.Item
// label={t("parts_orders.fields.db_price")}
// key={`${index}db_price`}
// name={[field.name, "db_price"]}
// >
// <CurrencyInput />
// </Form.Item>
}
<Form.Item
label={t("parts_orders.fields.quantity")}
key={`${index}quantity`}
name={[field.name, "quantity"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("parts_orders.fields.act_price")}
key={`${index}act_price`}
name={[field.name, "act_price"]}
>
<CurrencyInput />
</Form.Item>
{isReturn && (
<div style={{ display: "flex" }}>
<LayoutFormRow grow noDivider style={{ flex: 1 }}>
<Form.Item
label={t("parts_orders.fields.cost")}
key={`${index}cost`}
name={[field.name, "cost"]}
//span={8}
label={t("parts_orders.fields.line_desc")}
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("parts_orders.fields.line_remarks")}
key={`${index}line_remarks`}
name={[field.name, "line_remarks"]}
>
<Input />
</Form.Item>
<Form.Item
label={t("parts_orders.fields.oem_partno")}
key={`${index}oem_partno`}
name={[field.name, "oem_partno"]}
>
<Input />
</Form.Item>
{
// <Form.Item
// label={t("parts_orders.fields.db_price")}
// key={`${index}db_price`}
// name={[field.name, "db_price"]}
// >
// <CurrencyInput />
// </Form.Item>
}
<Form.Item
label={t("parts_orders.fields.quantity")}
key={`${index}quantity`}
name={[field.name, "quantity"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("parts_orders.fields.act_price")}
key={`${index}act_price`}
name={[field.name, "act_price"]}
>
<CurrencyInput />
</Form.Item>
)}
<Space wrap align="center">
{isReturn && (
<Form.Item
label={t("parts_orders.fields.cost")}
key={`${index}cost`}
name={[field.name, "cost"]}
>
<CurrencyInput />
</Form.Item>
)}
</LayoutFormRow>
<Space wrap size="small" align="center">
<div>
<DeleteFilled
style={{ margin: "1rem" }}
@@ -167,7 +175,7 @@ export function PartsOrderModalComponent({
total={fields.length}
/>
</Space>
</LayoutFormRow>
</div>
</Form.Item>
))}
</div>

View File

@@ -12,6 +12,7 @@ import ProductionAlert from "../production-list-columns/production-list-columns.
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
import "./production-board-card.styles.scss";
import moment from "moment";
export default function ProductionBoardCard(
technician,
@@ -37,6 +38,15 @@ export default function ProductionBoardCard(
// employee_csr = bodyshop.employees.find((e) => e.id === card.employee_csr);
// }
const pastDueAlert =
!!card.scheduled_completion &&
((moment().isSameOrAfter(moment(card.scheduled_completion), "day") &&
"production-completion-past") ||
(moment()
.add(1, "day")
.isSame(moment(card.scheduled_completion), "day") &&
"production-completion-soon"));
return (
<Card
className="react-kanban-card imex-kanban-card"
@@ -145,7 +155,7 @@ export default function ProductionBoardCard(
cardSettings.scheduled_completion &&
card.scheduled_completion && (
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
<Space>
<Space className={pastDueAlert}>
<CalendarOutlined />
<DateTimeFormatter format="MM/DD">
{card.scheduled_completion}

View File

@@ -2,7 +2,7 @@ import { useApolloClient } from "@apollo/client";
import Board, { moveCard } from "@asseinfo/react-kanban";
//import "@asseinfo/react-kanban/dist/styles.css";
import "./production-board-kanban.styles.scss";
import { SyncOutlined } from '@ant-design/icons'
import { SyncOutlined } from "@ant-design/icons";
import { Grid, notification, Button, PageHeader, Space, Statistic } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -49,9 +49,16 @@ export function ProductionBoardKanbanComponent({
const { t } = useTranslation();
useEffect(() => {
setBoardLanes(
createBoardData(bodyshop.md_ro_statuses.production_statuses, data, filter)
const boardData = createBoardData(
bodyshop.md_ro_statuses.production_statuses,
data,
filter
);
boardData.columns = boardData.columns.map((d) => {
return { ...d, title: `${d.title} (${d.cards.length})` };
});
setBoardLanes(boardData);
setIsMoving(false);
}, [
data,

View File

@@ -18,6 +18,7 @@ import ProductionListLastContacted from "./production-list-columns.lastcontacted
import ProductionListColumnPaintPriority from "./production-list-columns.paintpriority.component";
import ProductionListColumnNote from "./production-list-columns.productionnote.component";
import ProductionListColumnStatus from "./production-list-columns.status.component";
import ProductionListColumnCategory from "./production-list-columns.status.category";
import ProductionlistColumnTouchTime from "./prodution-list-columns.touchtime.component";
const r = ({ technician, state, activeStatuses, bodyshop }) => {
@@ -108,7 +109,7 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
state.sortedInfo.columnKey === "scheduled_completion" &&
state.sortedInfo.order,
render: (text, record) => (
<ProductionListDate record={record} field="scheduled_completion" />
<ProductionListDate record={record} field="scheduled_completion" pastIndicator />
),
},
{
@@ -155,7 +156,7 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
state.sortedInfo.columnKey === "scheduled_delivery" &&
state.sortedInfo.order,
render: (text, record) => (
<ProductionListDate record={record} field="scheduled_delivery" />
<ProductionListDate record={record} field="scheduled_delivery" pastIndicator/>
),
},
{
@@ -251,6 +252,29 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => <ProductionListColumnStatus record={record} />,
},
{
title: i18n.t("jobs.fields.category"),
dataIndex: "category",
key: "category",
ellipsis: true,
filters:
(bodyshop &&
bodyshop.md_categories.map((s) => {
return {
text: s,
value: [s],
};
})) ||
[],
onFilter: (value, record) => value.includes(record.category),
sorter: (a, b) => alphaSort(a.category, b.category),
sortOrder:
state.sortedInfo.columnKey === "category" && state.sortedInfo.order,
render: (text, record) => (
<ProductionListColumnCategory record={record} />
),
},
{
title: i18n.t("production.labels.bodyhours"),
dataIndex: "labhrs",

View File

@@ -1,22 +1,28 @@
import { useMutation } from "@apollo/client";
import { DatePicker, Dropdown, TimePicker, Button, Card } from "antd";
import { Button, Card, Dropdown, TimePicker } from "antd";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { DateFormatter } from "../../utils/DateFormatter";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import { useTranslation } from "react-i18next";
const OneCalendarDay = 60 * 60 * 24 * 1000;
const Now = new Date();
export default function ProductionListDate({ record, field, time }) {
export default function ProductionListDate({
record,
field,
time,
pastIndicator,
}) {
const [updateAlert] = useMutation(UPDATE_JOB);
const [visible, setVisible] = useState(false);
const { t } = useTranslation();
const handleChange = (date) => {
logImEXEvent("product_toggle_date", { field });
if (date.isSame(record[field] && moment(record[field]))) {
return;
}
//e.stopPropagation();
updateAlert({
@@ -34,6 +40,15 @@ export default function ProductionListDate({ record, field, time }) {
});
};
let className = "";
if (pastIndicator) {
className =
!!record[field] &&
((moment().isSameOrAfter(moment(record[field]), "day") &&
"production-completion-past") ||
(moment().add(1, "day").isSame(moment(record[field]), "day") &&
"production-completion-soon"));
}
return (
<div>
<Dropdown
@@ -47,7 +62,7 @@ export default function ProductionListDate({ record, field, time }) {
style={{ padding: "1rem" }}
onClick={(e) => e.stopPropagation()}
>
<DatePicker
<FormDatePicker
onClick={(e) => e.stopPropagation()}
value={(record[field] && moment(record[field])) || null}
onChange={handleChange}
@@ -72,17 +87,9 @@ export default function ProductionListDate({ record, field, time }) {
style={{
height: "19px",
}}
className={className}
>
<DateFormatter
bordered={false}
className={
!!record[field] && new Date(record[field]) - Now < OneCalendarDay
? "production-completion-1"
: ""
}
>
{record[field]}
</DateFormatter>
<DateFormatter bordered={false}>{record[field]}</DateFormatter>
</div>
</Dropdown>
</div>

View File

@@ -0,0 +1,63 @@
import { useMutation } from "@apollo/client";
import { Dropdown, Menu, Spin } from "antd";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export function ProductionListColumnCategory({ record, bodyshop }) {
const [updateJob] = useMutation(UPDATE_JOB);
const [loading, setLoading] = useState(false);
const handleSetStatus = async (e) => {
logImEXEvent("production_change_status");
setLoading(true);
const { key } = e;
await updateJob({
variables: {
jobId: record.id,
job: {
category: key,
},
},
});
setLoading(false);
};
return (
<Dropdown
overlay={
<Menu
style={{ maxHeight: "200px", overflowY: "auto" }}
onClick={handleSetStatus}
>
{bodyshop.md_categories.map((item) => (
<Menu.Item key={item}>{item}</Menu.Item>
))}
</Menu>
}
trigger={["click"]}
>
<div style={{ width: "100%", height: "19px", cursor: "pointer" }}>
{record.category}
{loading && <Spin />}
</div>
</Dropdown>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ProductionListColumnCategory);

View File

@@ -4,9 +4,25 @@ import { TemplateList } from "../../utils/TemplateConstants";
import { useTranslation } from "react-i18next";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const ProdTemplates = TemplateList("production");
const { production_by_technician_one, production_by_category_one } =
TemplateList("special");
export default function ProductionListPrint() {
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ProductionListPrint);
export function ProductionListPrint({ bodyshop }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
return (
@@ -33,6 +49,52 @@ export default function ProductionListPrint() {
{ProdTemplates[key].title}
</Menu.Item>
))}
<Menu.SubMenu
title={t("reportcenter.templates.production_by_technician_one")}
>
{bodyshop.employees.map((e) => (
<Menu.Item
key={e.id}
onClick={async () => {
setLoading(true);
await GenerateDocument(
{
name: production_by_technician_one.key,
variables: { id: e.id },
},
{},
"p"
);
setLoading(false);
}}
>
{e.first_name} {e.last_name}
</Menu.Item>
))}
</Menu.SubMenu>
<Menu.SubMenu
title={t("reportcenter.templates.production_by_category_one")}
>
{bodyshop.md_categories.map((e) => (
<Menu.Item
key={e}
onClick={async () => {
setLoading(true);
await GenerateDocument(
{
name: production_by_category_one.key,
variables: { category: e },
},
{},
"p"
);
setLoading(false);
}}
>
{e}
</Menu.Item>
))}
</Menu.SubMenu>
</Menu>
}
>

View File

@@ -88,6 +88,12 @@ export function ProductionListTable({
);
const handleTableChange = (pagination, filters, sorter) => {
console.log(
"🚀 ~ file: production-list-table.component.jsx ~ line 91 ~ pagination, filters, sorter",
pagination,
filters,
sorter
);
setState({
...state,
filteredInfo: filters,
@@ -265,6 +271,7 @@ export function ProductionListTable({
columns={columns.map((c, index) => {
return {
...c,
filteredValue: state.filteredInfo[c.key] || null,
sortOrder:
state.sortedInfo.columnKey === c.key && state.sortedInfo.order,
title: headerItem(c),

View File

@@ -25,15 +25,20 @@ export function ScheduleCalendarHeaderGraph({ bodyshop, loadData }) {
const { ssbuckets } = bodyshop;
const data = useMemo(() => {
return Object.keys(loadData.expectedLoad).map((key) => {
const metadataBucket = ssbuckets.filter((b) => b.id === key)[0];
return (
(loadData &&
loadData.expectedLoad &&
Object.keys(loadData.expectedLoad).map((key) => {
const metadataBucket = ssbuckets.filter((b) => b.id === key)[0];
return {
bucket: loadData.expectedLoad[key].label,
current: loadData.expectedLoad[key].count,
target: metadataBucket && metadataBucket.target,
};
});
return {
bucket: loadData.expectedLoad[key].label,
current: loadData.expectedLoad[key].count,
target: metadataBucket && metadataBucket.target,
};
})) ||
[]
);
}, [loadData, ssbuckets]);
const popContent = (

View File

@@ -7,6 +7,8 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import ShopEmployeesFormComponent from "./shop-employees-form.component";
import ShopEmployeesListComponent from "./shop-employees-list.component";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
@@ -21,11 +23,13 @@ function ShopEmployeesContainer({ bodyshop }) {
return (
<div>
<ShopEmployeesListComponent
employees={data ? data.employees : []}
loading={loading}
/>
<ShopEmployeesFormComponent />
<RbacWrapper action="employees:page">
<ShopEmployeesListComponent
employees={data ? data.employees : []}
loading={loading}
/>
<ShopEmployeesFormComponent />
</RbacWrapper>
</div>
);
}

View File

@@ -75,9 +75,17 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<div>
{(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) && (
<>
<DataLabel label={t("bodyshop.labels.dms.cdk_dealerid")}>
{form.getFieldValue("cdk_dealerid")}
</DataLabel>
{bodyshop.cdk_dealerid && (
<DataLabel label={t("bodyshop.labels.dms.cdk_dealerid")}>
{form.getFieldValue("cdk_dealerid")}
</DataLabel>
)}
{bodyshop.pbs_serialnumber && (
<DataLabel label={t("bodyshop.labels.dms.pbs_serialnumber")}>
{form.getFieldValue("pbs_serialnumber")}
</DataLabel>
)}
<LayoutFormRow>
<Form.Item
label={t("bodyshop.fields.dms.default_journal")}
@@ -158,11 +166,11 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
label={t("jobs.fields.dms.payer.control_type")}
key={`${index}control_type`}
name={[field.name, "control_type"]}
rules={[
{
required: true,
},
]}
// rules={[
// {
// required: true,
// },
// ]}
>
<Select showSearch>
<Select.Option value="ro_number">

View File

@@ -49,9 +49,13 @@ export function TechClockInComponent({ form, bodyshop, technician }) {
<Select>
{emps &&
emps.rates.map((item) => (
<Select.Option key={item.cost_center}>
<Select.Option key={item.cost_center} value={item.cost_center}>
{item.cost_center === "timetickets.labels.shift"
? t(item.cost_center)
: bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
? t(
`joblines.fields.lbr_types.${item.cost_center.toUpperCase()}`
)
: item.cost_center}
</Select.Option>
))}

View File

@@ -1,5 +1,5 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Form, notification } from "antd";
import { Button, Card, Form, notification, Space } from "antd";
import axios from "axios";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -9,6 +9,7 @@ import { INSERT_NEW_TIME_TICKET } from "../../graphql/timetickets.queries";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import TechClockInComponent from "./tech-job-clock-in-form.component";
import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component";
const mapStateToProps = createStructuredSelector({
technician: selectTechnician,
@@ -36,14 +37,17 @@ export function TechClockInContainer({ technician, bodyshop }) {
clockon: theTime,
jobid: values.jobid,
cost_center: values.cost_center,
ciecacode: Object.keys(
bodyshop.md_responsibility_centers.defaults.costs
).find((key) => {
return (
bodyshop.md_responsibility_centers.defaults.costs[key] ===
values.cost_center
);
}),
ciecacode:
bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
? values.cost_center
: Object.keys(
bodyshop.md_responsibility_centers.defaults.costs
).find((key) => {
return (
bodyshop.md_responsibility_centers.defaults.costs[key] ===
values.cost_center
);
}),
},
],
},
@@ -68,9 +72,16 @@ export function TechClockInContainer({ technician, bodyshop }) {
<Card
title={t("timetickets.labels.clockintojob")}
extra={
<Button type="primary" onClick={() => form.submit()} loading={loading}>
{t("timetickets.actions.clockin")}
</Button>
<Space wrap>
<TechJobPrintTickets />
<Button
type="primary"
onClick={() => form.submit()}
loading={loading}
>
{t("timetickets.actions.clockin")}
</Button>
</Space>
}
>
<Form form={form} layout="vertical" onFinish={handleFinish}>

View File

@@ -55,6 +55,20 @@ export function TechClockOffButton({
timeticket: {
clockoff: (await axios.post("/utils/time")).data,
...values,
rate: emps && emps.rates.filter(
(r) => r.cost_center === values.cost_center
)[0]?.rate,
ciecacode:
bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
? values.cost_center
: Object.keys(
bodyshop.md_responsibility_centers.defaults.costs
).find((key) => {
return (
bodyshop.md_responsibility_centers.defaults.costs[key] ===
values.cost_center
);
}),
},
},
});
@@ -141,6 +155,10 @@ export function TechClockOffButton({
<Select.Option key={item.cost_center}>
{item.cost_center === "timetickets.labels.shift"
? t(item.cost_center)
: bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
? t(
`joblines.fields.lbr_types.${item.cost_center.toUpperCase()}`
)
: item.cost_center}
</Select.Option>
))

View File

@@ -0,0 +1,125 @@
import { Button, Card, DatePicker, Form, Popover, Space } from "antd";
import moment from "moment";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import DatePIckerRanges from "../../utils/DatePickerRanges";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
const mapStateToProps = createStructuredSelector({
technician: selectTechnician,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(TechJobPrintTickets);
export function TechJobPrintTickets({ technician, event }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
const [visibility, setVisibility] = useState(false);
useEffect(() => {
if (visibility && event) {
form.setFieldsValue(event);
}
}, [visibility, form, event]);
const handleFinish = async (values) => {
logImEXEvent("schedule_manual_event");
setLoading(true);
const start = values.dates[0];
const end = values.dates[1];
try {
await GenerateDocument(
{
name: TemplateList().timetickets_employee.key,
variables: {
...(start
? { start: moment(start).startOf("day").format("YYYY-MM-DD") }
: {}),
...(end
? { end: moment(end).endOf("day").format("YYYY-MM-DD") }
: {}),
...(start ? { starttz: moment(start).startOf("day") } : {}),
...(end ? { endtz: moment(end).endOf("day") } : {}),
id: technician.id,
},
},
{
to: technician.email,
subject: TemplateList().timetickets_employee.subject,
},
"p"
);
} catch (error) {
console.log(error);
} finally {
setLoading(false);
setVisibility(false);
form.resetFields();
}
};
const overlay = (
<Card>
<div>
<Form form={form} layout="vertical" onFinish={handleFinish}>
<Form.Item
label={t("reportcenter.labels.dates")}
name="dates"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<DatePicker.RangePicker
ranges={DatePIckerRanges}
format={"MM/DD/YYYY"}
/>
</Form.Item>
<Space wrap>
<Button type="primary" onClick={() => form.submit()}>
{t("general.actions.print")}
</Button>
<Button
onClick={() => {
setVisibility(false);
form.resetFields();
}}
>
{t("general.actions.cancel")}
</Button>
</Space>
</Form>
</div>
</Card>
);
const handleClick = (e) => {
setVisibility(true);
};
return (
<Popover content={overlay} visible={visibility}>
<Button loading={loading} onClick={handleClick}>
{t("general.actions.print")}
</Button>
</Popover>
);
}

View File

@@ -32,7 +32,9 @@ export default function VendorsFormComponent({
return (
<div>
<PageHeader
title={form.getFieldValue("name")}
title={
<Form.Item shouldUpdate>{() => form.getFieldValue("name")}</Form.Item>
}
extra={
<Space>
<Form.Item

View File

@@ -262,6 +262,9 @@ export const QUERY_DELIVER_CHECKLIST = gql`
jobs_by_pk(id: $jobId) {
id
ro_number
actual_completion
actual_delivery
}
}
`;

View File

@@ -181,7 +181,10 @@ export const UPDATE_JOB_LINE = gql`
export const GET_JOB_LINES_TO_ENTER_BILL = gql`
query GET_JOB_LINES_TO_ENTER_BILL($id: uuid!) {
joblines(where: { jobid: { _eq: $id } }) {
joblines(
where: { jobid: { _eq: $id } }
order_by: { act_price: desc_nulls_last }
) {
removed
id
line_desc

View File

@@ -123,6 +123,7 @@ export const QUERY_EXACT_JOB_IN_PRODUCTION = gql`
ro_number
ownr_fn
ownr_ln
category
ownr_co_nm
v_model_yr
v_model_desc
@@ -193,6 +194,7 @@ export const QUERY_EXACT_JOBS_IN_PRODUCTION = gql`
status
ro_number
ownr_fn
category
ownr_ln
ownr_co_nm
v_model_yr
@@ -263,6 +265,7 @@ export const QUERY_JOBS_IN_PRODUCTION = gql`
id
updated_at
status
category
ro_number
ownr_fn
ownr_ln
@@ -1875,6 +1878,7 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
scheduled_delivery
actual_delivery
scheduled_in
date_invoiced
actual_in
kmin
kmout

View File

@@ -5,14 +5,14 @@ export const GLOBAL_SEARCH_QUERY = gql`
search_jobs(args: { search: $search }) {
id
ro_number
status
clm_total
clm_no
v_model_yr
v_model_desc
v_make_desc
v_color
plate_no
ownr_fn
ownr_ln
ownr_co_nm

View File

@@ -56,7 +56,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
jobId: job.id,
job: {
status: bodyshop.md_ro_statuses.default_invoiced || "",
date_invoiced: new Date(),
date_invoiced: values.date_invoiced,
actual_in: values.actual_in,
actual_completion: values.actual_completion,
actual_delivery: values.actual_delivery,
@@ -119,6 +119,9 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
actual_delivery: job.actual_delivery
? moment(job.actual_delivery)
: job.scheduled_delivery && moment(job.scheduled_delivery),
date_invoiced: job.date_invoiced
? moment(job.date_invoiced)
: moment(),
kmin: job.kmin,
kmout: job.kmout,
dms_allocation: job.dms_allocation,
@@ -219,6 +222,32 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
>
<DateTimePicker disabled={jobRO} />
</Form.Item>
<Form.Item
label={t("jobs.fields.date_invoiced")}
name="date_invoiced"
rules={[
{
required: true,
},
({ getFieldValue }) => ({
validator(_, value) {
if (!bodyshop.cdk_dealerid) return Promise.resolve();
if (!value || moment(value).isSameOrAfter(moment(), "day")) {
return Promise.resolve();
}
return Promise.reject(
new Error(t("jobs.labels.dms.invoicedatefuture"))
);
},
}),
]}
>
<DateTimePicker
disabled={jobRO}
onlyFuture={!!bodyshop.cdk_dealerid}
/>
</Form.Item>
{(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) && (
<Form.Item
label={t("jobs.fields.kmin")}

View File

@@ -69,6 +69,7 @@ export function JobsDeliverContainer({
checklistConfig={
(data && data.bodyshops_by_pk.deliverchecklist) || {}
}
job={data ? data.jobs_by_pk : {}}
/>
</div>
</RbacWrapper>

View File

@@ -270,7 +270,7 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
factory.client(payload.imexshopid);
const authRecord = payload.associations.filter(
(a) => a.useremail === userEmail
(a) => a.useremail.toLowerCase() === userEmail.toLowerCase()
);
yield put(setAuthlevel(authRecord[0] ? authRecord[0].authlevel : 0));

View File

@@ -1506,7 +1506,9 @@
"difference": "Difference",
"diskscan": "Scan Disk for Estimates",
"dms": {
"defaultstory": "Bodyshop RO {{ro_number}}. Damage to $t(jobs.fields.area_of_damage_impact.{{area_of_damage}}).",
"damageto": "Damage to $t(jobs.fields.area_of_damage_impact.{{area_of_damage}}).",
"defaultstory": "B/S RO: {{ro_number}}. Owner: {{ownr_nm}}. Insurance Co: {{ins_co_nm}}. Claim/PO #: {{clm_po}}",
"invoicedatefuture": "Invoice date must be today or in the future for CDK posting.",
"kmoutnotgreaterthankmin": "Mileage out must be greater than mileage in.",
"logs": "Logs",
"notallocated": "Not Allocated",
@@ -2219,6 +2221,7 @@
"attendance_summary": "Attendance Summary (All Employees)",
"credits_not_received_date": "Credits not Received by Date",
"csi": "CSI Responses",
"estimates_written_converted": "Estimates Written/Converted",
"estimator_detail": "Jobs by Estimator (Detail)",
"estimator_summary": "Jobs by Estimator (Summary)",
"export_payables": "Export Log - Payables",
@@ -2260,12 +2263,15 @@
"parts_not_recieved": "Parts Not Received",
"payments_by_date": "Payments by Date",
"payments_by_date_type": "Payments by Date and Type",
"production_by_category": "Production by Category",
"production_by_category_one": "Production filtered by Category",
"production_by_csr": "Production by CSR",
"production_by_last_name": "Production by Last Name",
"production_by_repair_status": "Production by Status",
"production_by_ro": "Production by RO",
"production_by_target_date": "Production by Target Date",
"production_by_technician": "Production by Technician",
"production_by_technician_one": "Production filtered by Technician",
"purchases_by_cost_center_detail": "Purchases by Cost Center (Detail)",
"purchases_by_cost_center_summary": "Purchases by Cost Center (Summary)",
"purchases_by_date_range_detail": "Purchases by Date - Detail",

View File

@@ -1506,7 +1506,9 @@
"difference": "",
"diskscan": "",
"dms": {
"damageto": "",
"defaultstory": "",
"invoicedatefuture": "",
"kmoutnotgreaterthankmin": "",
"logs": "",
"notallocated": "",
@@ -2219,6 +2221,7 @@
"attendance_summary": "",
"credits_not_received_date": "",
"csi": "",
"estimates_written_converted": "",
"estimator_detail": "",
"estimator_summary": "",
"export_payables": "",
@@ -2260,12 +2263,15 @@
"parts_not_recieved": "",
"payments_by_date": "",
"payments_by_date_type": "",
"production_by_category": "",
"production_by_category_one": "",
"production_by_csr": "",
"production_by_last_name": "",
"production_by_repair_status": "",
"production_by_ro": "",
"production_by_target_date": "",
"production_by_technician": "",
"production_by_technician_one": "",
"purchases_by_cost_center_detail": "",
"purchases_by_cost_center_summary": "",
"purchases_by_date_range_detail": "",

View File

@@ -1506,7 +1506,9 @@
"difference": "",
"diskscan": "",
"dms": {
"damageto": "",
"defaultstory": "",
"invoicedatefuture": "",
"kmoutnotgreaterthankmin": "",
"logs": "",
"notallocated": "",
@@ -2219,6 +2221,7 @@
"attendance_summary": "",
"credits_not_received_date": "",
"csi": "",
"estimates_written_converted": "",
"estimator_detail": "",
"estimator_summary": "",
"export_payables": "",
@@ -2260,12 +2263,15 @@
"parts_not_recieved": "",
"payments_by_date": "",
"payments_by_date_type": "",
"production_by_category": "",
"production_by_category_one": "",
"production_by_csr": "",
"production_by_last_name": "",
"production_by_repair_status": "",
"production_by_ro": "",
"production_by_target_date": "",
"production_by_technician": "",
"production_by_technician_one": "",
"purchases_by_cost_center_detail": "",
"purchases_by_cost_center_summary": "",
"purchases_by_date_range_detail": "",

View File

@@ -1466,6 +1466,24 @@ export const TemplateList = (type, context) => {
},
group: "customers",
},
estimates_written_converted: {
title: i18n.t("reportcenter.templates.estimates_written_converted"),
description: "",
subject: i18n.t(
"reportcenter.templates.estimates_written_converted"
),
key: "estimates_written_converted",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field:
i18n.t("jobs.fields.date_open") +
"/" +
i18n.t("jobs.fields.date_invoiced"), // Also date invoice.
},
group: "sales",
},
}
: {}),
...(!type || type === "courtesycarcontract"
@@ -1590,6 +1608,14 @@ export const TemplateList = (type, context) => {
//idtype: "vendor",
disabled: false,
},
production_by_category: {
title: i18n.t("reportcenter.templates.production_by_category"),
description: "",
subject: i18n.t("reportcenter.templates.production_by_category"),
key: "production_by_category",
//idtype: "vendor",
disabled: false,
},
production_by_technician: {
title: i18n.t("reportcenter.templates.production_by_technician"),
description: "",
@@ -1609,6 +1635,28 @@ export const TemplateList = (type, context) => {
key: "ca_bc_etf_table",
disabled: false,
},
production_by_technician_one: {
title: i18n.t(
"reportcenter.templates.production_by_technician_one"
),
description: "",
subject: i18n.t(
"reportcenter.templates.production_by_technician_one"
),
key: "production_by_technician_one",
//idtype: "vendor",
disabled: false,
},
production_by_category_one: {
title: i18n.t("reportcenter.templates.production_by_category_one"),
description: "",
subject: i18n.t(
"reportcenter.templates.production_by_category_one"
),
key: "production_by_category_one",
//idtype: "vendor",
disabled: false,
},
}
: {}),
};

View File

@@ -68,6 +68,10 @@ app.get("/test", async function (req, res) {
"git rev-parse --short HEAD"
);
logger.log("test-api-status", "DEBUG", "api", { commit });
sendEmail.sendServerEmail({
subject: `API Check - ${process.env.NODE_ENV}`,
text: `Server API check has come in. `,
});
res.status(200).send(`OK - ${commit}`);
});

View File

@@ -30,7 +30,7 @@ axios.interceptors.request.use((x) => {
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
console.log(printable);
CdkBase.createLogEvent(socket, "TRACE", `Raw Request: ${printable}`);
CdkBase.createJsonEvent(socket, "TRACE", `Raw Request: ${printable}`, x.data);
return x;
});
@@ -42,7 +42,12 @@ axios.interceptors.response.use((x) => {
x.data
)}`;
console.log(printable);
CdkBase.createLogEvent(socket, "TRACE", `Raw Response: ${printable}`);
CdkBase.createJsonEvent(
socket,
"TRACE",
`Raw Response: ${printable}`,
x.data
);
return x;
});
@@ -60,7 +65,11 @@ exports.default = async function (socket, { txEnvelope, jobid }) {
const JobData = await QueryJobData(socket, jobid);
socket.JobData = JobData;
CdkBase.createLogEvent(
socket,
"DEBUG",
`Querying the DMS for the Vehicle Record.`
);
//Query for the Vehicle record to get the associated customer.
socket.DmsVeh = await QueryVehicleFromDms(socket);
//Todo: Need to validate the lines and methods below.
@@ -116,7 +125,11 @@ exports.PbsSelectedCustomer = async function PbsSelectedCustomer(
);
await UpsertVehicleData(socket, ownerRef.ReferenceId);
CdkBase.createLogEvent(socket, "DEBUG", `Inserting account data.`);
CdkBase.createLogEvent(
socket,
"DEBUG",
`Inserting accounting posting data..`
);
await InsertAccountPostingData(socket);
CdkBase.createLogEvent(socket, "DEBUG", `Marking job as exported.`);

View File

@@ -38,7 +38,9 @@ exports.default = function ({
if (
(jobline.prt_dsmk_p && jobline.prt_dsmk_p !== 0) ||
((jobline.db_ref === "900511" || jobline.db_ref === "900510") &&
((jobline.db_ref === "900511" ||
jobline.db_ref === "900510" ||
jobline.db_ref === "900500") &&
jobline.prt_dsmk_m &&
jobline.prt_dsmk_m !== 0)
) {

View File

@@ -76,9 +76,7 @@ const generateBill = (bill) => {
DueDate:
bill.due_date && moment(bill.due_date).format("YYYY-MM-DD"),
RefNumber: bill.invoice_number,
Memo: `RO ${bill.job.ro_number || ""} OWNER ${
bill.job.ownr_fn || ""
} ${bill.job.ownr_ln || ""} ${bill.job.ownr_co_nm || ""}`,
Memo: `RO ${bill.job.ro_number || ""}`,
ExpenseLineAdd: bill.billlines.map((il) =>
generateBillLine(
il,

View File

@@ -72,7 +72,9 @@ exports.default = async function (socket, jobid) {
if (
(val.prt_dsmk_p && val.prt_dsmk_p !== 0) ||
((val.db_ref === "900511" || val.db_ref === "900510") &&
((val.db_ref === "900511" ||
val.db_ref === "900510" ||
val.db_ref === "900500") &&
val.prt_dsmk_m &&
val.prt_dsmk_m !== 0)
) {

View File

@@ -2,6 +2,7 @@ const path = require("path");
const queries = require("../graphql-client/queries");
const Dinero = require("dinero.js");
const moment = require("moment");
const fs = require("fs");
const _ = require("lodash");
const logger = require("../utils/logger");
@@ -28,7 +29,6 @@ exports.default = async (req, res) => {
logger.log("arms-start", "DEBUG", "api", null, null);
const { bodyshops } = await client.request(queries.GET_ENTEGRAL_SHOPS);
const allxmlsToUpload = [];
const allErrors = [];
try {
for (const bodyshop of bodyshops) {
@@ -40,10 +40,12 @@ exports.default = async (req, res) => {
const { jobs } = await client.request(queries.ENTEGRAL_EXPORT, {
bodyshopid: bodyshop.id,
});
const ret = jobs.map((job) => {
const jobsToPush = [];
jobs.forEach((job) => {
const transId = uuid(); // Can this actually be the job id?
return {
let obj = {
RqUID: transId,
DocumentInfo: {
BMSVer: "4.0.0",
@@ -55,16 +57,18 @@ exports.default = async (req, res) => {
TransmitDateTime: moment().format(momentFormat), // Omitted from ARMS docs
},
EventInfo: {
AssignmentEvent: {
CreateDateTime:
job.asgn_date && moment(job.asgn_date).format(momentFormat),
},
EstimateEvent: {
UploadDateTime: moment().format(momentFormat),
},
// AssignmentEvent: {
// CreateDateTime:
// job.asgn_date && moment(job.asgn_date).format(momentFormat),
// },
// EstimateEvent: {
// UploadDateTime: moment().format(momentFormat),
// },
RepairEvent: {
CreatedDateTime:
job.date_open && moment(job.date_open).format(momentFormat),
CreatedDateTime: (job.date_open
? moment(job.date_open)
: moment()
).format(momentFormat),
ArrivalDateTime:
job.actual_in && moment(job.actual_in).format(momentFormat),
ArrivalOdometerReading: job.kmin,
@@ -92,35 +96,35 @@ exports.default = async (req, res) => {
IDQualifierCode: "US",
IDNum: 44, // ** Not sure where to get this entegral ID from?
},
Communications: [
{
CommQualifier: "WA",
Address: {
Address1: job.ins_addr1,
Address2: job.ins_addr2,
City: job.ins_city,
StateProvince: job.ins_st,
PostalCode: job.ins_zip,
CountryCode: job.ins_ctry,
},
},
{
CommQualifier: "WP",
CommPhone: job.ins_ph1,
},
{
CommQualifier: "WF",
CommPhone: job.ins_ph2,
},
],
},
ContactInfo: {
ContactJobTitle: "Adjuster",
ContactName: {
FirstName: job.est_ct_fn,
LastName: job.est_ct_ln,
},
// Communications: [
// {
// CommQualifier: "WA",
// Address: {
// Address1: job.ins_addr1,
// Address2: job.ins_addr2,
// City: job.ins_city,
// StateProvince: job.ins_st,
// PostalCode: job.ins_zip,
// CountryCode: job.ins_ctry,
// },
// },
// {
// CommQualifier: "WP",
// CommPhone: job.ins_ph1,
// },
// {
// CommQualifier: "WF",
// CommPhone: job.ins_ph2,
// },
// ],
},
// ContactInfo: {
// ContactJobTitle: "Adjuster",
// ContactName: {
// FirstName: job.est_ct_fn,
// LastName: job.est_ct_ln,
// },
// },
},
},
// InsuranceAgent: {
@@ -141,16 +145,16 @@ exports.default = async (req, res) => {
// },
// },
// },
Insured: {
Party: {
PersonInfo: {
PersonName: {
FirstName: job.insd_fn,
LastName: job.insd_ln,
},
},
},
},
// Insured: {
// Party: {
// PersonInfo: {
// PersonName: {
// FirstName: job.insd_fn,
// LastName: job.insd_ln,
// },
// },
// },
// },
Owner: {
Party: {
PersonInfo: {
@@ -158,68 +162,70 @@ exports.default = async (req, res) => {
FirstName: job.ownr_fn,
LastName: job.ownr_ln,
},
Communications: [
{
CommQualifier: "HA",
Address: {
Address1: job.ownr_addr1,
// Communications: [
// {
// CommQualifier: "HA",
// Address: {
// Address1: job.ownr_addr1,
City: job.ownr_city,
StateProvince: job.ownr_st,
PostalCode: job.ownr_zip,
CountryCode: job.ownr_ctry,
},
},
{
CommQualifier: "HP",
CommPhone: job.ownr_ph1,
},
{
CommQualifier: "WP",
CommPhone: job.ownr_ph2,
},
{
CommQualifier: "CP",
CommPhone: job.ownr_ph1,
},
{
CommQualifier: "EM",
CommEmail: job.ownr_ea,
},
],
},
},
},
Claimant: {
Party: {
PersonInfo: {
PersonName: {
FirstName: job.clm_ct_fn,
LastName: job.clm_ct_ln,
},
},
},
OwnerInd: true,
},
Estimator: {
Party: {
PersonInfo: {
PersonName: {
FirstName: job.est_ct_fn,
LastName: job.est_ct_ln,
},
// IDInfo: {
// IDQualifierCode: "US",
// IDNum: 2941,
// },
// City: job.ownr_city,
// StateProvince: job.ownr_st,
// PostalCode: job.ownr_zip,
// CountryCode: job.ownr_ctry,
// },
// },
// {
// CommQualifier: "HP",
// CommPhone: job.ownr_ph1,
// },
// {
// CommQualifier: "WP",
// CommPhone: job.ownr_ph2,
// },
// {
// CommQualifier: "CP",
// CommPhone: job.ownr_ph1,
// },
// {
// CommQualifier: "EM",
// CommEmail: job.ownr_ea,
// },
// ],
},
},
},
// Claimant: {
// Party: {
// PersonInfo: {
// PersonName: {
// FirstName: job.clm_ct_fn,
// LastName: job.clm_ct_ln,
// },
// },
// },
// OwnerInd: true,
// },
// Estimator: {
// Party: {
// PersonInfo: {
// PersonName: {
// FirstName: job.est_ct_fn,
// LastName: job.est_ct_ln,
// },
// // IDInfo: {
// // IDQualifierCode: "US",
// // IDNum: 2941,
// // },
// },
// },
// },
RepairFacility: {
//This section not in documentation.
Party: {
OrgInfo: {
CompanyName: bodyshop.shopname,
CompanyName:
process.env.NODE_ENV === "production"
? bodyshop.shopname
: "IMEX Test Shop",
IDInfo: {
IDQualifierCode: "US",
IDNum: bodyshop.entegral_id,
@@ -233,7 +239,7 @@ exports.default = async (req, res) => {
// VendorCode: "C",
// EstimateDocumentID: "1223HJ76",
},
//RepairOrderType: "DRP",
RepairOrderType: "DirectRepairProgram", //Need to get from Entegral
//ReferralSourceType: "Yellow Pages",
VehicleInfo: {
VINInfo: {
@@ -241,9 +247,9 @@ exports.default = async (req, res) => {
VINNum: job.v_vin,
},
},
License: {
LicensePlateNum: job.plate_no,
},
// License: {
// LicensePlateNum: job.plate_no,
// },
VehicleDesc: {
//ProductionDate: "2009-10",
ModelYear:
@@ -255,23 +261,23 @@ exports.default = async (req, res) => {
MakeDesc: job.v_make_desc,
ModelName: job.v_model_desc,
},
Paint: {
Exterior: {
Color: {
ColorName: job.v_color,
// OEMColorCode: "1M3",
},
},
},
// Paint: {
// Exterior: {
// Color: {
// ColorName: job.v_color,
// // OEMColorCode: "1M3",
// },
// },
// },
// Body: {
// BodyStyle: "2 Door Convertible",
// Trim: {
// TrimCode: "1B3",
// },
// },
Condition: {
DrivableInd: job.driveable ? "Y" : "N",
},
// Condition: {
// DrivableInd: job.driveable ? "Y" : "N",
// },
},
ClaimInfo: {
ClaimNum: job.clm_no,
@@ -296,7 +302,7 @@ exports.default = async (req, res) => {
},
},
ProfileInfo: {
//ProfileName: "Shop Standard Rates",
ProfileName: "ImEX",
RateInfo: [
{
RateType: "PA",
@@ -306,7 +312,7 @@ exports.default = async (req, res) => {
TaxableInd: true,
TaxTierInfo: {
TierNum: 1,
Percentage: 0, //TODO Find the best place to take the tax rates for parts.
Percentage: job.parts_tax_rates.PAN.prt_tax_rt * 100, //TODO Find the best place to take the tax rates for parts.
},
},
},
@@ -318,7 +324,7 @@ exports.default = async (req, res) => {
TaxableInd: true,
TaxTierInfo: {
TierNum: 1,
Percentage: 0, //TODO Find the best place to take the tax rates for labor.
Percentage: job.parts_tax_rates.PAN.prt_tax_rt * 100, //TODO Find the best place to take the tax rates for labor.
},
},
},
@@ -495,56 +501,56 @@ exports.default = async (req, res) => {
TotalType: "PAA",
TotalTypeDesc: "Aftermarket Parts",
TotalAmt: Dinero(
job.job_totals.parts.parts.list.paa &&
job.job_totals.parts.parts.list.paa.total
job.job_totals.parts.parts.list.PAA &&
job.job_totals.parts.parts.list.PAA.total
).toFormat("0.00"),
},
{
TotalType: "PAC",
TotalTypeDesc: "Re-Chromed Parts",
TotalAmt: Dinero(
job.job_totals.parts.parts.list.pac &&
job.job_totals.parts.parts.list.pac.total
job.job_totals.parts.parts.list.PAC &&
job.job_totals.parts.parts.list.PAC.total
).toFormat("0.00"),
},
{
TotalType: "PAG",
TotalTypeDesc: "Glass Parts",
TotalAmt: Dinero(
job.job_totals.parts.parts.list.pag &&
job.job_totals.parts.parts.list.pag.total
job.job_totals.parts.parts.list.PAG &&
job.job_totals.parts.parts.list.PAG.total
).toFormat("0.00"),
},
{
TotalType: "PAL",
TotalTypeDesc: "LKQ/Used Parts",
TotalAmt: Dinero(
job.job_totals.parts.parts.list.pal &&
job.job_totals.parts.parts.list.pal.total
job.job_totals.parts.parts.list.PAL &&
job.job_totals.parts.parts.list.PAL.total
).toFormat("0.00"),
},
{
TotalType: "PAM",
TotalTypeDesc: "Remanufactured Parts",
TotalAmt: Dinero(
job.job_totals.parts.parts.list.pam &&
job.job_totals.parts.parts.list.pam.total
job.job_totals.parts.parts.list.PAM &&
job.job_totals.parts.parts.list.PAM.total
).toFormat("0.00"),
},
{
TotalType: "PAN",
TotalTypeDesc: "New Parts",
TotalAmt: Dinero(
job.job_totals.parts.parts.list.pan &&
job.job_totals.parts.parts.list.pan.total
job.job_totals.parts.parts.list.PAN &&
job.job_totals.parts.parts.list.PAN.total
).toFormat("0.00"),
},
{
TotalType: "PAR",
TotalTypeDesc: "Recored Parts",
TotalAmt: Dinero(
job.job_totals.parts.parts.list.par &&
job.job_totals.parts.parts.list.par.total
job.job_totals.parts.parts.list.PAR &&
job.job_totals.parts.parts.list.PAR.total
).toFormat("0.00"),
},
],
@@ -616,6 +622,14 @@ exports.default = async (req, res) => {
"0.00"
),
},
{
TotalType: "TOT",
TotalSubType: "SM",
TotalTypeDesc: "Supplement Total",
TotalAmt: job.cieca_ttl
? job.cieca_ttl.data.supp_amt
: Dinero().toFormat("0.00"),
},
{
TotalType: "TOT",
TotalSubType: "F7",
@@ -632,12 +646,14 @@ exports.default = async (req, res) => {
"0.00"
),
},
// {
// TotalType: "TOT",
// TotalSubType: "SM",
// TotalTypeDesc: "Supplement Total",
// TotalAmt: 0,
// },
{
TotalType: "TOT",
TotalSubType: "D8",
TotalTypeDesc: "Bottom Line Discount",
TotalAmt: Dinero(
job.job_totals.additional.adjustments
).toFormat("0.00"),
},
{
TotalType: "TOT",
TotalSubType: "D2",
@@ -658,15 +674,13 @@ exports.default = async (req, res) => {
TotalType: "TOT",
TotalSubType: "AA",
TotalTypeDesc: "Appearance Allowance",
TotalAmt: 0,
TotalAmt: Dinero().toFormat("0.00"),
},
{
TotalType: "TOT",
TotalSubType: "D8",
TotalTypeDesc: "Bottom Line Discount",
TotalAmt: Dinero(
job.job_totals.additional.adjustments
).toFormat("0.00"),
TotalSubType: "DEPOSIT",
TotalTypeDesc: "Deposit",
TotalAmt: Dinero().toFormat("0.00"),
},
{
TotalType: "TOT",
@@ -676,12 +690,6 @@ exports.default = async (req, res) => {
.subtract(Dinero(job.job_totals.totals.custPayable.total))
.toFormat("0.00"),
},
// {
// TotalType: "TOT",
// TotalSubType: "DEPOSIT",
// TotalTypeDesc: "Deposit",
// TotalAmt: 0,
// },
{
TotalType: "TOT",
TotalSubType: "CUST",
@@ -691,7 +699,7 @@ exports.default = async (req, res) => {
).toFormat("0.00"),
},
],
RepairTotalsType: 1,
// RepairTotalsType: 1,
},
// RepairLabor: {
// LaborAllocations: {
@@ -741,7 +749,7 @@ exports.default = async (req, res) => {
// },
ProductionStatus: {
ProductionStage: {
ProductionStageCode: GetProductionStageCode(job),
ProductionStageCode: GetProductionStageCode(job, bodyshop),
ProductionStageDateTime: moment().format(momentFormat),
// ProductionStageStatusComment:
// "Going to be painted this afternoon",
@@ -777,6 +785,9 @@ exports.default = async (req, res) => {
// },
// },
};
deleteNullKeys(obj);
jobsToPush.push(obj);
});
if (erroredJobs.length > 0) {
@@ -784,13 +795,12 @@ exports.default = async (req, res) => {
count: erroredJobs.length,
jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number)),
});
allErrors = [...allErrors, ...erroredJobs];
}
logger.log("arms-end-shop-extract", "DEBUG", "api", bodyshop.id, {
shopname: bodyshop.shopname,
});
const abc = ret[1];
deleteNullKeys(abc);
try {
const entegralSoapClient = await soap.createClientAsync(
@@ -798,7 +808,7 @@ exports.default = async (req, res) => {
{
ignoredNamespaces: true,
wsdl_options: {
useEmptyTag: true,
// useEmptyTag: true,
},
wsdl_headers: {
Authorization: `Basic ${new Buffer.from(
@@ -816,14 +826,20 @@ exports.default = async (req, res) => {
);
const entegralResponse =
await entegralSoapClient.RepairOrderFolderAddRqAsync(abc);
await entegralSoapClient.RepairOrderFolderAddRqAsync(
jobsToPush,
function (err, result, rawResponse, soapHeader, rawRequest) {
fs.writeFileSync(`./logs/arms-request.xml`, rawRequest);
fs.writeFileSync(`./logs/arms-response.xml`, rawResponse);
res.json(err || result);
}
);
const [result, rawResponse, , rawRequest] = entegralResponse;
console.log("🚀 ~ file: arms.js ~ line 806 ~ result", result);
res.json({ result, obj: abc });
} catch (error) {
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
console.log(error);
res.json(error);
}
} catch (error) {
//Error at the shop level.
@@ -846,15 +862,18 @@ exports.default = async (req, res) => {
}
}
// res.sendStatus(200);
res.sendStatus(200);
} catch (error) {
res.status(200).json(error);
}
};
function GetSupplementNumber(joblines) {
return 0;
return _.max(joblines.map((jl) => jl.line_ind));
const max = _.max(
joblines.map((jl) => parseInt((jl.line_ind || "0").replace(/[^\d.-]/g, "")))
);
return max || 0;
}
function GetDocumentstatus(job, bodyshop) {
@@ -873,7 +892,9 @@ function GetDocumentstatus(job, bodyshop) {
function GetRepairStatusCode(job) {
return "25";
}
function GetProductionStageCode(job) {
function GetProductionStageCode(job, bodyshop) {
if (bodyshop.md_ro_statuses.post_production_statuses.includes(job.status))
return "8D";
return "33";
}

View File

@@ -15,6 +15,7 @@ require("dotenv").config({
let Client = require("ssh2-sftp-client");
const client = require("../graphql-client/graphql-client").client;
const { sendServerEmail } = require("../email/sendemail");
const AHDineroFormat = "0.00";
const AhDateFormat = "MMDDYYYY";
@@ -37,25 +38,34 @@ exports.default = async (req, res) => {
logger.log("autohouse-start", "DEBUG", "api", null, null);
const { bodyshops } = await client.request(queries.GET_AUTOHOUSE_SHOPS);
const specificShopIds = req.body.bodyshopIds; // ['uuid]
const start = req.body.start; //YYYY-MM-DD
const allxmlsToUpload = [];
const allErrors = [];
try {
for (const bodyshop of bodyshops) {
for (const bodyshop of specificShopIds
? bodyshops.filter((b) => specificShopIds.includes(b.id))
: bodyshops) {
logger.log("autohouse-start-shop-extract", "DEBUG", "api", bodyshop.id, {
shopname: bodyshop.shopname,
});
const erroredJobs = [];
try {
const { jobs } = await client.request(queries.AUTOHOUSE_QUERY, {
bodyshopid: bodyshop.id,
start: moment().subtract(3, "days").startOf("day"),
});
const { jobs, bodyshops_by_pk } = await client.request(
queries.AUTOHOUSE_QUERY,
{
bodyshopid: bodyshop.id,
start: start
? moment(start).startOf("day")
: moment().subtract(3, "days").startOf("day"),
}
);
const autoHouseObject = {
AutoHouseExport: {
RepairOrder: jobs.map((j) =>
CreateRepairOrderTag(
{ ...j, bodyshop },
{ ...j, bodyshop: bodyshops_by_pk },
function ({ job, error }) {
erroredJobs.push({ job: job, error: error.toString() });
}
@@ -113,15 +123,13 @@ exports.default = async (req, res) => {
}
}
//if (process.env.NODE_ENV !== "production") {
for (const xmlObj of allxmlsToUpload) {
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
}
res.json(allxmlsToUpload);
return;
// for (const xmlObj of allxmlsToUpload) {
// fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
// }
// res.json(allxmlsToUpload);
// return;
let sftp = new Client();
sftp.on("error", (errors) =>
logger.log("autohouse-sftp-error", "ERROR", "api", null, {
@@ -155,7 +163,17 @@ exports.default = async (req, res) => {
} finally {
sftp.end();
}
sendServerEmail({
subject: `Autohouse Report ${moment().format("MM-DD-YY")}`,
text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
Uploaded: ${JSON.stringify(
allxmlsToUpload.map((x) => x.filename),
null,
2
)}
`,
});
res.sendStatus(200);
} catch (error) {
res.status(200).json(error);
@@ -278,10 +296,10 @@ const CreateRepairOrderTag = (job, errorCallback) => {
AssntoMech: null,
AssntoPaint: null,
AssntoDetail: null,
PromiseDate:
(job.scheduled_completion &&
moment(job.scheduled_completion).format(AhDateFormat)) ||
"",
// PromiseDate:
// (job.scheduled_completion &&
// moment(job.scheduled_completion).format(AhDateFormat)) ||
// "",
//InsuranceTargetOut: null,
CarComplete:
(job.actual_completion &&
@@ -312,6 +330,11 @@ const CreateRepairOrderTag = (job, errorCallback) => {
RefinishRate: job.rate_lar || 0,
MechanicalRate: job.rate_lam || 0,
StructuralRate: job.rate_las || 0,
ElectricalRate: job.rate_lae || 0,
FrameRate: job.rate_laf || 0,
GlassRate: job.rate_lag || 0,
DetailRate: job.rate_lad || 0,
LaborMiscRate: 0,
PMRate: job.rate_mapa || 0,
BMRate: job.rate_mash || 0,
TaxRate:
@@ -389,16 +412,25 @@ const CreateRepairOrderTag = (job, errorCallback) => {
// AmountDue: null,
// },
RevisedTotals: {
BodyHours: job.job_totals.rates.lab.hours,
BodyHours: job.job_totals.rates.lab.hours.toFixed(2),
BodyRepairHours: job.joblines
.filter((line) => repairOpCodes.includes(line.lbr_op))
.reduce((acc, val) => acc + val.mod_lb_hrs, 0),
.reduce((acc, val) => acc + val.mod_lb_hrs, 0)
.toFixed(2),
BodyReplaceHours: job.joblines
.filter((line) => replaceOpCodes.includes(line.lbr_op))
.reduce((acc, val) => acc + val.mod_lb_hrs, 0),
RefinishHours: job.job_totals.rates.lar.hours,
MechanicalHours: job.job_totals.rates.lam.hours,
StructuralHours: job.job_totals.rates.las.hours,
.reduce((acc, val) => acc + val.mod_lb_hrs, 0)
.toFixed(2),
RefinishHours: job.job_totals.rates.lar.hours.toFixed(2),
MechanicalHours: job.job_totals.rates.lam.hours.toFixed(2),
StructuralHours: job.job_totals.rates.las.hours.toFixed(2),
ElectricalHours: job.job_totals.rates.lae.hours.toFixed(2),
FrameHours: job.job_totals.rates.laf.hours.toFixed(2),
GlassHours: job.job_totals.rates.lag.hours.toFixed(2),
DetailHours: job.job_totals.rates.lad.hours.toFixed(2),
LaborMiscHours: 0,
PartsTotal: Dinero(job.job_totals.parts.parts.total).toFormat(
AHDineroFormat
),
@@ -459,16 +491,39 @@ const CreateRepairOrderTag = (job, errorCallback) => {
),
StructuralLaborTotalCost:
repairCosts.StructuralLaborTotalCost.toFormat(AHDineroFormat),
ElectricalLaborTotal: Dinero(job.job_totals.rates.lae.total).toFormat(
AHDineroFormat
),
ElectricalLaborTotalCost:
repairCosts.ElectricalLaborTotalCost.toFormat(AHDineroFormat),
FrameLaborTotal: Dinero(job.job_totals.rates.laf.total).toFormat(
AHDineroFormat
),
FrameLaborTotalCost:
repairCosts.FrameLaborTotalCost.toFormat(AHDineroFormat),
GlassLaborTotal: Dinero(job.job_totals.rates.lag.total).toFormat(
AHDineroFormat
),
GlassLaborTotalCost:
repairCosts.GlassLaborTotalCost.toFormat(AHDineroFormat),
DetailLaborTotal: Dinero(job.job_totals.rates.lag.total).toFormat(
AHDineroFormat
),
DetailLaborTotalCost:
repairCosts.DetailLaborTotalCost.toFormat(AHDineroFormat),
LaborMiscTotal: 0,
LaborMiscTotalCost: 0,
MiscellaneousChargeTotal: 0,
MiscellaneousChargeTotalCost: 0,
PMTotal: Dinero(job.job_totals.rates.mapa.total).toFormat(
AHDineroFormat
),
PMTotalCost: 0,
PMTotalCost: repairCosts.PMTotalCost.toFormat(AHDineroFormat),
BMTotal: Dinero(job.job_totals.rates.mash.total).toFormat(
AHDineroFormat
),
BMTotalCost: 0,
BMTotalCost: repairCosts.BMTotalCost.toFormat(AHDineroFormat),
MiscTotal: 0,
MiscTotalCost: 0,
TowingTotal: Dinero(job.job_totals.additional.towing).toFormat(
@@ -647,6 +702,12 @@ const CreateCosts = (job) => {
ticketTotalsByCostCenter[defaultCosts.LAM] || Dinero(),
StructuralLaborTotalCost:
ticketTotalsByCostCenter[defaultCosts.LAS] || Dinero(),
ElectricalLaborTotalCost:
ticketTotalsByCostCenter[defaultCosts.LAE] || Dinero(),
FrameLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAF] || Dinero(),
GlassLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAG] || Dinero(),
DetailLaborTotalCost:
ticketTotalsByCostCenter[defaultCosts.LAD] || Dinero(),
PMTotalCost: billTotalsByCostCenters[defaultCosts.MAPA] || Dinero(),
BMTotalCost: billTotalsByCostCenters[defaultCosts.MASH] || Dinero(),
MiscTotalCost: billTotalsByCostCenters[defaultCosts.PAO] || Dinero(),
@@ -679,7 +740,7 @@ const StatusMapping = (status, md_ro_statuses) => {
else if (status === default_delivered) return "DEL";
else if (status === default_invoiced || status === default_exported)
return "CLO";
else if (status === default_void) return "CLO";
else if (status === default_void) return "VOID";
else if (md_ro_statuses.production_statuses.includes(status)) return "IPR";
else return "UNDEFINED";
@@ -689,12 +750,7 @@ const StatusMapping = (status, md_ro_statuses) => {
const GenerateDetailLines = (line, statuses) => {
const ret = {
BackOrdered: line.status === statuses.default_bo ? "1" : "0",
Cost:
(line.billlines[0] &&
(line.billlines[0].actual_cost * line.billlines[0].quantity).toFixed(
2
)) ||
0,
Cost: (line.billlines[0] && line.billlines[0].actual_cost.toFixed(2)) || 0,
//Critical: null,
Description: line.line_desc || "",
DiscountMarkup: line.prt_dsmk_m || 0,
@@ -702,27 +758,39 @@ const GenerateDetailLines = (line, statuses) => {
IOUPart: 0,
LineNumber: line.line_no || 0,
MarkUp: null,
OrderedOn: null,
OrderedOn:
(line.parts_order_lines[0] &&
moment(line.parts_order_lines[0].parts_order.order_date).format(
AhDateFormat
)) ||
"",
OriginalCost: null,
OriginalInvoiceNumber: null,
PriceEach: (line.billlines[0] && line.billlines[0].retail_price) || 0,
PriceEach: line.act_price || 0,
PartNumber: _.escape(line.oem_partno),
ProfitPercent: null,
PurchaseOrderNumber: null,
Qty: line.part_qty || 0,
Status: line.status || "",
SupplementNumber: line.line_ind || "",
SupplementNumber: line.line_ind ? line.line_ind.replace(/[^\d.-]/g, "") : 0,
Type: line.part_type || "",
Vendor: (line.billlines[0] && line.billlines[0].bill.vendor.name) || "",
VendorPaid: null,
VendorPrice: (line.billlines[0] && line.billlines[0].actual_price) || 0,
VendorPrice:
(line.billlines[0] &&
(line.billlines[0].actual_price * line.billlines[0].quantity).toFixed(
2
)) ||
0,
Deleted: null,
ExpectedOn: null,
ReceivedOn: null,
ReceivedOn:
line.billlines[0] &&
moment(line.billlines[0].bill.date).format(AhDateFormat),
OrderedBy: null,
ShipVia: null,
VendorContact: null,
EstimateAmount: line.act_price * line.part_qty || 0, //Rebecca
EstimateAmount: (line.act_price * line.part_qty).toFixed(2) || 0, //Rebecca
};
return ret;
};
@@ -738,7 +806,7 @@ const generateNullDetailLine = () => {
IOUPart: 0,
LineNumber: 0,
MarkUp: null,
OrderedOn: null,
OrderedOn: "",
OriginalCost: null,
OriginalInvoiceNumber: null,
PriceEach: 0,

View File

@@ -18,6 +18,34 @@ let transporter = nodemailer.createTransport({
SES: { ses, aws },
});
exports.sendServerEmail = async function ({ subject, text }) {
try {
transporter.sendMail(
{
from: `ImEX Online API - ${process.env.NODE_ENV} <noreply@imex.online>`,
to: ["patrick@snapt.ca"],
subject: subject,
text: text,
ses: {
// optional extra arguments for SendRawEmail
Tags: [
{
Name: "tag_name",
Value: "tag_value",
},
],
},
},
(err, info) => {
console.log(err || info);
}
);
} catch (error) {
console.log(error);
logger.log("server-email-failure", "error", null, null, error);
}
};
exports.sendEmail = async (req, res) => {
logger.log("send-email", "DEBUG", req.user.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`,

View File

@@ -543,6 +543,23 @@ exports.QUERY_EMPLOYEE_PIN = `query QUERY_EMPLOYEE_PIN($shopId: uuid!, $employee
}`;
exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshopid: uuid!) {
bodyshops_by_pk(id: $bodyshopid){
id
shopname
address1
city
state
zip_post
country
phone
md_ro_statuses
md_order_statuses
autohouseid
md_responsibility_centers
jc_hourly_rates
}
jobs(where: {_and: [{converted: {_eq: true}}, {updated_at: {_gt: $start}}, {shopid: {_eq: $bodyshopid}}]}) {
id
ro_number
@@ -608,21 +625,7 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshop
job_totals
driveable
parts_tax_rates
bodyshop {
id
shopname
address1
city
state
zip_post
country
phone
md_ro_statuses
md_order_statuses
autohouseid
md_responsibility_centers
jc_hourly_rates
}
joblines(where: {removed: {_eq: false}}) {
id
line_no
@@ -642,7 +645,13 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshop
lbr_op
profitcenter_part
profitcenter_labor
billlines(order_by: {bill: {date: desc_nulls_last}}) {
parts_order_lines(order_by: {parts_order: {order_date: desc_nulls_last}} limit: 1){
parts_order{
id
order_date
}
}
billlines(order_by: {bill: {date: desc_nulls_last}} limit: 1) {
actual_cost
actual_price
quantity
@@ -651,6 +660,7 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshop
name
}
invoice_number
date
}
}
}
@@ -695,8 +705,6 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshop
}
}
}
`;
exports.ENTEGRAL_EXPORT = `
@@ -783,6 +791,8 @@ query ENTEGRAL_EXPORT($bodyshopid: uuid!) {
rate_matd
job_totals
ded_amt
cieca_ttl
adjustment_bottom_line
}
}
`;
@@ -1030,6 +1040,7 @@ exports.QUERY_JOB_COSTING_DETAILS = ` query QUERY_JOB_COSTING_DETAILS($id: uuid!
status
ca_bc_pvrt
ca_customer_gst
dms_allocation
joblines(where: { removed: { _eq: false } }) {
id
db_ref
@@ -1073,11 +1084,14 @@ exports.QUERY_JOB_COSTING_DETAILS = ` query QUERY_JOB_COSTING_DETAILS($id: uuid!
actualhrs
productivehrs
flat_rate
ciecacode
}
bodyshop{
id
md_responsibility_centers
jc_hourly_rates
cdk_dealerid
pbs_serialnumber
}
}
}`;
@@ -1133,6 +1147,7 @@ exports.QUERY_JOB_COSTING_DETAILS_MULTI = ` query QUERY_JOB_COSTING_DETAILS_MULT
status
ca_bc_pvrt
ca_customer_gst
dms_allocation
joblines(where: {removed: {_eq: false}}) {
id
db_ref
@@ -1176,11 +1191,14 @@ exports.QUERY_JOB_COSTING_DETAILS_MULTI = ` query QUERY_JOB_COSTING_DETAILS_MULT
actualhrs
productivehrs
flat_rate
ciecacode
}
bodyshop {
id
md_responsibility_centers
jc_hourly_rates
cdk_dealerid
pbs_serialnumber
}
}
}

View File

@@ -261,7 +261,7 @@ function GenerateCostingData(job) {
val.profitcenter_labor || defaultProfits[val.mod_lbr_ty] || "?";
if (laborProfitCenter === "?")
console.log("Unknown type", val.mod_lbr_ty);
console.log("Unknown type", val.line_desc, val.mod_lbr_ty);
const rateName = `rate_${(val.mod_lbr_ty || "").toLowerCase()}`;
const laborAmount = Dinero({
@@ -285,11 +285,12 @@ function GenerateCostingData(job) {
val.profitcenter_part || defaultProfits[val.part_type] || "?";
if (partsProfitCenter === "?")
console.log("Unknown type", val.part_type);
console.log("Unknown type", val.line_desc, val.part_type);
if (!partsProfitCenter)
console.log(
"Unknown cost/profit center mapping for parts.",
val.line_desc,
val.part_type
);
const partsAmount = Dinero({
@@ -298,13 +299,13 @@ function GenerateCostingData(job) {
.multiply(val.part_qty || 1)
.add(
val.prt_dsmk_m && val.prt_dsmk_m !== 0
? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) })
: Dinero({
amount: Math.round(val.act_price * 100),
})
.multiply(val.part_qty || 0)
.percentage(Math.abs(val.prt_dsmk_p || 0))
.multiply(val.prt_dsmk_p > 0 ? 1 : -1)
? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) })
: Dinero({
amount: Math.round(val.act_price * 100),
})
.multiply(val.part_qty || 0)
.percentage(Math.abs(val.prt_dsmk_p || 0))
.multiply(val.prt_dsmk_p > 0 ? 1 : -1)
);
if (!acc.parts[partsProfitCenter])
acc.parts[partsProfitCenter] = Dinero();
@@ -322,7 +323,7 @@ function GenerateCostingData(job) {
"?";
if (partsProfitCenter === "?") {
console.log("Unknown type", val.part_type);
console.log("Unknown type", val.line_desc, val.part_type);
} else {
const partsAmount = Dinero({
amount: Math.round((val.act_price || 0) * 100),
@@ -330,13 +331,13 @@ function GenerateCostingData(job) {
.multiply(val.part_qty || 1)
.add(
val.prt_dsmk_m && val.prt_dsmk_m !== 0
? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) })
: Dinero({
amount: Math.round(val.act_price * 100),
})
.multiply(val.part_qty || 0)
.percentage(Math.abs(val.prt_dsmk_p || 0))
.multiply(val.prt_dsmk_p > 0 ? 1 : -1)
? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) })
: Dinero({
amount: Math.round(val.act_price * 100),
})
.multiply(val.part_qty || 0)
.percentage(Math.abs(val.prt_dsmk_p || 0))
.multiply(val.prt_dsmk_p > 0 ? 1 : -1)
);
if (!acc.parts[partsProfitCenter])
@@ -372,21 +373,43 @@ function GenerateCostingData(job) {
);
}
//Is it a DMS Setup?
const selectedDmsAllocationConfig =
(job.bodyshop.md_responsibility_centers.dms_defaults &&
job.bodyshop.md_responsibility_centers.dms_defaults.find(
(d) => d.name === job.dms_allocation
)) ||
job.bodyshop.md_responsibility_centers.defaults;
const billTotalsByCostCenters = job.bills.reduce((bill_acc, bill_val) => {
//At the bill level.
bill_val.billlines.map((line_val) => {
//At the bill line level.
//console.log("JobCostingPartsTable -> line_val", line_val);
if (!bill_acc[line_val.cost_center])
bill_acc[line_val.cost_center] = Dinero();
if (job.bodyshop.pbs_serialnumber || job.bodyshop.cdk_dealerid) {
if (!bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]])
bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]] =
Dinero();
bill_acc[line_val.cost_center] = bill_acc[line_val.cost_center].add(
Dinero({
amount: Math.round((line_val.actual_cost || 0) * 100),
})
.multiply(line_val.quantity)
.multiply(bill_val.is_credit_memo ? -1 : 1)
);
bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]] =
bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]].add(
Dinero({
amount: Math.round((line_val.actual_cost || 0) * 100),
})
.multiply(line_val.quantity)
.multiply(bill_val.is_credit_memo ? -1 : 1)
);
} else {
if (!bill_acc[line_val.cost_center])
bill_acc[line_val.cost_center] = Dinero();
bill_acc[line_val.cost_center] = bill_acc[line_val.cost_center].add(
Dinero({
amount: Math.round((line_val.actual_cost || 0) * 100),
})
.multiply(line_val.quantity)
.multiply(bill_val.is_credit_memo ? -1 : 1)
);
}
return null;
});
@@ -443,20 +466,42 @@ function GenerateCostingData(job) {
const ticketTotalsByCostCenter = job.timetickets.reduce(
(ticket_acc, ticket_val) => {
//At the invoice level.
if (!ticket_acc[ticket_val.cost_center])
ticket_acc[ticket_val.cost_center] = Dinero();
ticket_acc[ticket_val.cost_center] = ticket_acc[
ticket_val.cost_center
].add(
Dinero({
amount: Math.round((ticket_val.rate || 0) * 100),
}).multiply(
ticket_val.flat_rate
? ticket_val.productivehrs || ticket_val.actualhrs || 0
: ticket_val.actualhrs || ticket_val.productivehrs || 0
) //Should base this on the employee.
);
if (job.bodyshop.pbs_serialnumber || job.bodyshop.cdk_dealerid) {
if (
!ticket_acc[selectedDmsAllocationConfig.costs[ticket_val.ciecacode]]
)
ticket_acc[selectedDmsAllocationConfig.costs[ticket_val.ciecacode]] =
Dinero();
ticket_acc[selectedDmsAllocationConfig.costs[ticket_val.ciecacode]] =
ticket_acc[
selectedDmsAllocationConfig.costs[ticket_val.ciecacode]
].add(
Dinero({
amount: Math.round((ticket_val.rate || 0) * 100),
}).multiply(
ticket_val.flat_rate
? ticket_val.productivehrs || ticket_val.actualhrs || 0
: ticket_val.actualhrs || ticket_val.productivehrs || 0
) //Should base this on the employee.
);
} else {
if (!ticket_acc[ticket_val.cost_center])
ticket_acc[ticket_val.cost_center] = Dinero();
ticket_acc[ticket_val.cost_center] = ticket_acc[
ticket_val.cost_center
].add(
Dinero({
amount: Math.round((ticket_val.rate || 0) * 100),
}).multiply(
ticket_val.flat_rate
? ticket_val.productivehrs || ticket_val.actualhrs || 0
: ticket_val.actualhrs || ticket_val.productivehrs || 0
) //Should base this on the employee.
);
}
return ticket_acc;
},

View File

@@ -271,7 +271,8 @@ function CalculatePartsTotals(jobLines) {
.multiply(value.part_qty || 0)
.add(
(value.db_ref === "900511" ||
value.db_ref === "900510") &&
value.db_ref === "900510" ||
value.db_ref === "900500") &&
value.prt_dsmk_m &&
value.prt_dsmk_m !== 0
? Dinero({ amount: Math.round(value.prt_dsmk_m * 100) })
@@ -298,7 +299,9 @@ function CalculatePartsTotals(jobLines) {
parts: {
...acc.parts,
prt_dsmk_total: acc.parts.prt_dsmk_total.add(
(value.db_ref === "900511" || value.db_ref === "900510") &&
(value.db_ref === "900511" ||
value.db_ref === "900510" ||
value.db_ref === "900500") &&
value.prt_dsmk_m &&
value.prt_dsmk_m !== 0
? Dinero({ amount: Math.round(value.prt_dsmk_m * 100) })
@@ -344,7 +347,9 @@ function CalculatePartsTotals(jobLines) {
}).multiply(value.part_qty || 0)
)
.add(
(value.db_ref === "900511" || value.db_ref === "900510") &&
(value.db_ref === "900511" ||
value.db_ref === "900510" ||
value.db_ref === "900500") &&
value.prt_dsmk_m &&
value.prt_dsmk_m !== 0
? Dinero({ amount: Math.round(value.prt_dsmk_m * 100) })

View File

@@ -13,13 +13,20 @@ const logger = require("../utils/logger");
const client = twilio(
process.env.TWILIO_AUTH_TOKEN,
process.env.TWILIO_AUTH_KEY
);const { admin } = require("../firebase/firebase-handler");
);
const { admin } = require("../firebase/firebase-handler");
const gqlClient = require("../graphql-client/graphql-client").client;
exports.send = (req, res) => {
const { to, messagingServiceSid, body, conversationid, selectedMedia, imexshopid } =
req.body;
const {
to,
messagingServiceSid,
body,
conversationid,
selectedMedia,
imexshopid,
} = req.body;
logger.log("sms-outbound", "DEBUG", req.user.email, null, {
messagingServiceSid: messagingServiceSid,
@@ -34,7 +41,12 @@ exports.send = (req, res) => {
req.body.selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : [],
});
if (!!to && !!messagingServiceSid && !!body && !!conversationid) {
if (
!!to &&
!!messagingServiceSid &&
(!!body || !!selectedMedia.length > 0) &&
!!conversationid
) {
client.messages
.create({
body: body,
@@ -67,10 +79,9 @@ exports.send = (req, res) => {
const data = {
type: "messaging-outbound",
conversationid: newMessage.conversationid || "",
};
admin.messaging().send({
admin.messaging().send({
topic: `${imexshopid}-messaging`,
data,
});

View File

@@ -17,7 +17,10 @@ const CdkCalculateAllocations =
require("../cdk/cdk-calculate-allocations").default;
const { isArray } = require("lodash");
const logger = require("../utils/logger");
const {default: PbsExportJob, PbsSelectedCustomer} = require("../accounting/pbs/pbs-job-export");
const {
default: PbsExportJob,
PbsSelectedCustomer,
} = require("../accounting/pbs/pbs-job-export");
io.use(function (socket, next) {
try {
@@ -159,6 +162,36 @@ function createLogEvent(socket, level, message) {
}
}
function createJsonEvent(socket, level, message, json) {
if (LogLevelHierarchy(socket.log_level) >= LogLevelHierarchy(level)) {
console.log(
`[WS LOG EVENT] ${level} - ${new Date()} - ${socket.user.email} - ${
socket.id
} - ${message}`
);
socket.emit("log-event", {
timestamp: new Date(),
level,
message,
});
}
logger.log("ws-log-event-json", level, socket.user.email, socket.recordid, {
wsmessage: message,
json,
});
if (socket.logEvents && isArray(socket.logEvents)) {
socket.logEvents.push({
timestamp: new Date(),
level,
message,
});
}
// if (level === "ERROR") {
// throw new Error(message);
// }
}
function createXmlEvent(socket, xml, message, isError = false) {
if (LogLevelHierarchy(socket.log_level) >= LogLevelHierarchy("TRACE")) {
socket.emit("log-event", {
@@ -210,3 +243,4 @@ function LogLevelHierarchy(level) {
exports.createLogEvent = createLogEvent;
exports.createXmlEvent = createXmlEvent;
exports.createJsonEvent = createJsonEvent;